{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "ReC98",
  "home_page_url": "https://rec98.nmlgc.net/blog",
  "description": "The Touhou PC-98 Restoration Project",
  "items": [
    {
      "id": "https://rec98.nmlgc.net/blog/2025-12-31",
      "url": "https://rec98.nmlgc.net/blog/2025-12-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003cdiv title=\"Already at the latest blog post.\"\u003e🔼\u003c/div\u003e\u003ca href=\"#2025-10-19\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-12-31\"\u003e\u003ctime datetime=\"2025-12-31T22:43:39Z\"\u003e2025-12-31 22:43\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0327\"\u003eP0327\u003c/a\u003e\n\t\t\tTH03 RE (Character-specific attack function pointers / Bullet dependencies / Bullet structure)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3130b0a...f862533\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0328\"\u003eP0328\u003c/a\u003e\n\t\t\tTH03 decompilation (Bullets, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f862533...843d942\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0329\"\u003eP0329\u003c/a\u003e\n\t\t\tTH03 decompilation (Bullets, part 2) / Twitter→Fediverse migration, part 1\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/843d942...d892535\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/3d03d1c...8c4f526\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/twitter-archive-to-gotosocial/compare/36ad7fb...122f56f\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH03 gameplay research specific to Mima.\"\u003emima-th03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kana\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH03 gameplay research specific to Kana.\"\u003ekana\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/chiyuri\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH03 gameplay research specific to Chiyuri.\"\u003echiyuri\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u003cstyle\u003e\n\t.sprites-2025-12-31 th,\n\t.sprites-2025-12-31 td {\n\t\tbackground-color: silver;\n\t}\n\t.sprites-2025-12-31 tr\u003e:first-child {\n\t\tborder-right: var(--table-border);\n\t}\n\t.sprites-2025-12-31 tr:not(:last-child) {\n\t\tborder-bottom: var(--table-border);\n\t}\n\t.sprites-2025-12-31 .sprite {\n\t\tdisplay: block;  \n\t\tfloat: left;  \n\t\timage-rendering: pixelated;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tAlright! I've been announcing a big look at TH03's in-game systems all throughout 2025, and I technically still made it before the end of the year. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e TH03's enemy, fireball, and explosion systems are a great fit for this occasion: They fulfill both of the netplay-relevant criteria I mentioned \u003ca href=\"/blog/2025-10-19\"\u003e📝 at the end of the previous blog post\u003c/a\u003e, but also unfortunately share the same structure and overload some of their fields with vastly different meanings, much like \u003ca href=\"/blog/2020-02-29\"\u003e📝 TH04's and TH05's custom entities\u003c/a\u003e. Hence, they will take rather long to untangle, which ensures that the resulting look will be appropriately big.\n\u003c/p\u003e\u003cp\u003e\n\tUntil I noticed that explosions spawn bullets in a not-so-straightforward way that basically requires complete knowledge of the bullet system. What a great discovery to make 2½ pushes into development… Oh well, we have the budget, and bullets also happen to match our netplay-relevant criteria, so let's get those done first.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#gba-2025-12-31\"\u003eA cursory look at character-specific attacks\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#logic-2025-12-31\"\u003eTH03's bullet logic\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#overview-2025-12-31\"\u003eHigh-level overview\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#trail-2025-12-31\"\u003eTrail sprites\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#groups-2025-12-31\"\u003eRings and other groups\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#transfer-2025-12-31\"\u003eTransferred pellets\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#render-2025-12-31\"\u003eTH03's bullet renderer\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#pellets-2025-12-31\"\u003ePellets, and unresolved VRAM alignment questions\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#clouds-2025-12-31\"\u003eDelay clouds, and the unfortunate lack of a SPRITE16 feature\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#fedi-2025-12-31\"\u003eMigrating away from Twitter\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#downfall-2025-12-31\"\u003eTwitter's technical downfall (not clickbait)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#bluesky-2025-12-31\"\u003eTrying a Bluesky PDS\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#mastodon-2025-12-31\"\u003eTrying Mastodon\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#gotosocial-2025-12-31\"\u003eDeciding on GoToSocial as a Fediverse server\u003c/a\u003e \u003c/li\u003e\u003c/ul\u003e\u003c/ol\u003e\u003chr id=\"gba-2025-12-31\"\u003e\u003cp\u003e\n\tAs usual, we'd also like to identify and name all character-specific functions in the ASM code so that we can immediately correlate certain interesting features of the bullet system with the characters and attacks that use them. In TH03, this is particularly worthwhile because it's all we need for a 100% complete overview of how bullets are used. Apart from the transferred pellets fired from exploding enemies outside of Gauge or Boss Attacks, every bullet pattern in the game is part of such a hardcoded and character-specific Extra, Gauge, or Boss Attack, since enemy scripts cannot fire bullets in this game.\u003cbr\u003e\n\tThis quick look also showed how ZUN implemented the 9 characters in a highly consistent manner. Gauge Attacks in particular follow a predictable convention:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe \"Level 2\" attack (available at 50% gauge and consuming 25% gauge) always fires 8×8 pellets.\u003c/li\u003e\n\t\u003cli\u003eThe \"Level 3\" attack (available at 75% gauge and consuming 50% gauge) always fires 16×16 bullets.\u003c/li\u003e\n\t\u003cli\u003eThe game only provides a single function pointer for each of the two levels, which gets called as part of game logic and before the game starts rendering the current frame. With no room for custom rendering calls, characters can only define these attacks as patterns that are made up of common entities.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe funny part in all of this: All characters follow these conventions, yet ZUN still architected TH03 as if they don't. Each of the two Gauge Attack levels gets a separate per-player function pointer, but every character just uses these two functions to call a single common function with a flag that indicates Level 2 or Level 3. This common function then follows a similar structure for all 9 characters as well. The same trend continues with the Boss Attacks, where we find 9 copies of the more or less unchanged update and rendering boilerplate… so yeah, ZUN basically copy-pasted the same code 9 times with minor variations.\u003cbr\u003e\n\tAnd now we can be \u003ci\u003every\u003c/i\u003e hyped for the future of TH03 decompilation. Lots of duplicates of the same functionality means that I'll basically only have to decompile them once, which means that TH03 decompilation is very likely to progress \u003ci\u003every quickly\u003c/i\u003e in terms of absolute numbers once I get the basic gameplay systems done. And there's not a lot of \u003ci\u003ethat\u003c/i\u003e code left either: After this delivery, we're left with a mere 128 undecompiled foundational functions that are not related to any specific character. After the next delivery, that number will drop to \u003ci\u003e95\u003c/i\u003e. I'm expecting a return to the glorious days of 2020, where the 3 copies of TH01's foundational graphics code allowed me to decompile 10% of its entire code within \u003ca href=\"/progress/f2b454dfc61ed5359b4246e6562da0966eedf58d\"\u003e2½\u003c/a\u003e \u003ca href=\"/progress/7698f5da6d1b5d56550980a223994308b3f12ff2\"\u003eweeks\u003c/a\u003e. With 9 copies tripling that speed, we may even get to finish this game next year?\n\u003c/p\u003e\u003chr id=\"logic-2025-12-31\"\u003e\u003cp\u003e\n\tOnto bullets then! As you'd expect, TH03's bullet system is based on \u003ca href=\"/blog/2025-02-24\"\u003e📝 TH02's system we looked at earlier this year\u003c/a\u003e, which in turn was based on TH01's system. In some respects, it's a minor iteration of TH02's system adapted to the new features in TH03, but some of these new features also form the missing link between TH02 and \u003ca href=\"/blog/2020-02-16\"\u003e📝 TH04\u003c/a\u003e. The high-level overview:\n\u003c/p\u003e\u003cul id=\"overview-2025-12-31\"\u003e\u003cli\u003e\u003cp\u003e\n\tLike most entities in TH03, bullets are stored in a single array that is shared between both players. Each bullet has a structure field that denotes which playfield it is moving on and constrained to.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe total bullet cap shared among both players is 320, slightly more than twice the 150-bullet cap we saw in TH02.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tJust like in TH02, this system covers both 8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e pellets and 16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e sprite bullets. The former are once again hardcoded and rendered using the GRCG, while the latter are rendered by SPRITE16.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe system defines a default set of four adjacent 16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e bullet sprites, starting at (﻿64,\u0026nbsp;0﻿) within \u003ca href=\"/blog/2022-02-18\"\u003e📝 SPRITE16's sprite area\u003c/a\u003e. These can be used in two ways:\u003c/p\u003e\u003col\u003e\n\t\t\u003cli\u003eFour animation frames for a single bullet type, animated at the maximum speed of 1 cel per frame. This is how they are used by most characters.\u003c/li\u003e\n\t\t\u003cli\u003eAlternatively, they can represent one non-animated bullet type and three trail sprites, as seen in Mima's and Chiyuri's Boss Attacks.\u003c/li\u003e\n\t\u003c/ol\u003e\n\t\u003cp\u003ePatterns can override the default 16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e sprites with an arbitrary other set of four adjacent sprites, but this feature is only used in Kana's Extra Attack.\u003c/p\u003e\n\t\u003cfigure class=\"sprites-2025-12-31\"\u003e\u003ctable style=\"text-align: left;\"\u003e\n\t\t\t\u003cthead\u003e\n\t\t\t\t\u003ctr\u003e\u003cth\u003eCharacter\u003c/th\u003e\u003cth\u003eSprites\u003c/th\u003e\u003c/tr\u003e\n\t\t\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-0-Sheet.webp?76ada76a\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-0.webp?f13d3fa6\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Reimu's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc1.webp?c3a345a5\" alt=\"Mima\" width=\"24\" height=\"24\"\u003e Mima\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-1.webp?2fb682b7\" class=\"sprite\"\n\t\t\twidth=\"64\"\n\t\t\talt=\"TH03 Mima's single 16×16 bullet sprite, and the three trail sprites\"\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc2.webp?18385020\" alt=\"Marisa\" width=\"24\" height=\"24\"\u003e Marisa\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-2-Sheet.webp?7ee6c8be\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-2.webp?c951a24d\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Marisa's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc3.webp?9101ba3a\" alt=\"Ellen\" width=\"24\" height=\"24\"\u003e Ellen\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-3-Sheet.webp?75a49ea5\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-3.webp?038d5450\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Ellen's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc4.webp?91a4fd1b\" alt=\"Kotohime\" width=\"24\" height=\"24\"\u003e Kotohime\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-4-Sheet.webp?ecde9d9e\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-4.webp?ef726125\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Kotohime's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc5.webp?cba094a2\" alt=\"Kana\" width=\"24\" height=\"24\"\u003e Kana\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\n\t\t\t\t\t\t\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-5-Sheet.webp?22e2e52b\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-5.webp?99b7c883\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Kana's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\n\t\t\t\t\t\t\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-5-Extra-Sheet.webp?3556ac21\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-5-Extra.webp?4054b4d1\" class=\"sprite\"\n\t\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\t\talt=\"The animated 16×16 bullet sprites used in TH03 Kana's Extra Attack\"\n\t\t\t\t\t\t\u003e\u003c/a\u003e\n\t\t\t\t\t\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc6.webp?bfdfae8f\" alt=\"Rikako\" width=\"24\" height=\"24\"\u003e Rikako\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-6-Sheet.webp?7401d7a5\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-6.webp?2c75dbd4\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Rikako's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc7.webp?c77b71c0\" alt=\"Chiyuri\" width=\"24\" height=\"24\"\u003e Chiyuri\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-7.webp?f138c5ae\" class=\"sprite\"\n\t\t\twidth=\"64\"\n\t\t\talt=\"TH03 Chiyuri's single 16×16 bullet sprite, and the three trail sprites\"\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc8.webp?a83a4201\" alt=\"Yumemi\" width=\"24\" height=\"24\"\u003e Yumemi\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"/blog/static/2025-12-31-TH03-bullet-8-Sheet.webp?eab1c94c\"\u003e\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-8.webp?b9d584fb\" class=\"sprite\"\n\t\t\twidth=\"16\"\n\t\t\talt=\"TH03 Yumemi's animated 16×16 bullet sprites\"\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\n\t\t\t\u003c/tbody\u003e\n\t\t\u003c/table\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tThe SPRITE16 area of certain characters might contain other bullet-like sprites, but these are used by a different gameplay system than the one described in this post.\u003cbr\u003e\n\t\t\tI've reduced the animation speed to \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e its original length because 18\u0026nbsp;ms would look \u003ci\u003every\u003c/i\u003e obnoxious in the context of a web page. Run \u003ckbd\u003ewebpmux -duration 18ms\u003c/kbd\u003e on the animated WebP files to restore the original speed.\u003cbr\u003e\n\t\t\tAnd yes, Yumemi's sprite is animated \u003ci\u003every\u003c/i\u003e subtly. Click the animated sprites for the raw sprite sheet.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/li\u003e\u003cli\u003e\n\tAs we already found out \u003ca href=\"/blog/2022-02-18\"\u003e📝 in 2022\u003c/a\u003e, both 8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e pellets and 16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e bullets have the same \"hitbox\" – a single 2×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e2\u003c/span\u003e-pixel tile in the game's collision bitmap that gets compared against the 8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e square surrounding the player's center.\n\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tDelay clouds are back after their absence in TH02. They are still limited to pellets, though.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe \u003ca href=\"/blog/2020-02-16\"\u003e📝 bullet template\u003c/a\u003e makes its debut in this game, replacing TH01's and TH02's spawn function parameters with a single piece of global data. This introduces the usual trade-offs with this sort of thing: Code size savings in patterns that spawn multiple groups with minor variations to their parameters, in exchange for the usual confusion that comes with widely mutated global state. As a result, code quality suffers greatly, especially when it comes to the derived transfer pellets fired within the bullet system itself. Sure, there \u003ci\u003eare\u003c/i\u003e lots of patterns where retained state comes in handy, but local per-pattern template instances would have solved that as well.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThis decline in code quality also extends to the rest of the logic code. Continuing his \u003ca href=\"https://en.touhougarakuta.com/article/specialtaidan_zun_hiroyuki_2-en/\"\u003egeneral trend of micro-optimizing based purely on vibes\u003c/a\u003e we've seen \u003ca href=\"/blog/2022-02-18\"\u003e📝 time\u003c/a\u003e and \u003ca href=\"/blog/2024-04-24#hitcirc-2024-04-24\"\u003e📝 time\u003c/a\u003e again, ZUN wrote a significant part of TH03's bullet logic in ASM, especially in the update function. And once again, it's the same tragic conclusion: While TH04's and TH05's full-on ASM approach might later bring \u003ci\u003esome\u003c/i\u003e measurable runtime benefits to bullet logic via self-modifying code and whatnot, TH03 is left with pretty much only the downsides of its partial ASM approach. If ZUN just wrote idiomatic C++ code without any optimization tricks and inlined just one function, the whole bullet update code would have been 87 lines of C++ shorter and \u003ci\u003eexactly\u003c/i\u003e as large when compiled. And that's \u003ci\u003ewith\u003c/i\u003e all the redundant code still in place! TH02's not-great-but-passable implementation of bullets indeed marked the high point for the PC-98 series, and it only went downhill from there.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tThree features of the bullet system deserve a deeper look:\n\u003c/p\u003e\u003ch4 id=\"trail-2025-12-31\"\u003eTrail sprites\u003c/h4\u003e\u003cp\u003e\n\tThese work by remembering the last 6 positions of a bullet and rendering the sprites at the 2\u003csup\u003end\u003c/sup\u003e, 4\u003csup\u003eth\u003c/sup\u003e, and 6\u003csup\u003eth\u003c/sup\u003e position, respectively:\n\u003c/p\u003e\u003cfigure class=\"side_by_side pixelated\"\u003e\n\t\u003cfigure style=\"width: 288px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Mima-trail-pattern.webp?6390b73e\" preload=\"none\" controls loop width=\"288\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"78\" style=\"aspect-ratio: 288 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Mima-trail-pattern.avi?a2098aea\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Mima-trail-pattern.webm?d924a75e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Mima-trail-pattern.webm?9688b373\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Mima-trail-pattern.webm?dddf0bac\" type=\"video/webm\"\u003eVideo of TH03 Mima's one Boss Attack pattern whose bullets use the trail sprite feature of the game's bullet system, modified to create a better video loop. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Mima-trail-pattern.avi?a2098aea\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\n\t\u003cfigure style=\"width: 288px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Chiyuri-trail-pattern.webp?b971b7bd\" preload=\"none\" controls loop width=\"288\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"96\" style=\"aspect-ratio: 288 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Chiyuri-trail-pattern.avi?772b55f2\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Chiyuri-trail-pattern.webm?9b904be1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Chiyuri-trail-pattern.webm?0b5dc2d3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Chiyuri-trail-pattern.webm?1181ae01\" type=\"video/webm\"\u003eVideo of TH03 Chiyuri's one Boss Attack pattern whose bullets use the trail sprite feature of the game's bullet system. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Chiyuri-trail-pattern.avi?772b55f2\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tObviously, this requires (﻿\u003cspan class=\"hovertext\" title=\"Trail sprite count\"\u003e6\u003c/span\u003e\u0026nbsp;×\n\t\u003cspan class=\"hovertext\" title=\"X and Y\"\u003e2\u003c/span\u003e\u0026nbsp;×\n\t\u003cspan class=\"hovertext\" title=\"Byte size of a Q12.4 variable\"\u003e2\u003c/span\u003e﻿)\u0026nbsp;=\n\t24 additional bytes per bullet. Adding these to the regular bullet structure would waste\n\t(﻿24\u0026nbsp;× \u003cspan class=\"hovertext\" title=\"Bullet cap\"\u003e320\u003c/span\u003e﻿)\u0026nbsp;= 7,680 bytes of conventional RAM, which would in no way be justified for a feature that ZUN used in a grand total of two patterns. For once, ZUN agreed, and instead provided a single ring buffer that can hold these 6 additional positions for up to 48 bullets. This allowed ZUN to reduce the per-bullet cost to 3 bytes: 1 byte for the \u003cq\u003ehas trail\u003c/q\u003e flag, and 2 bytes for a \u003ccode\u003enear\u003c/code\u003e pointer into the ring buffer. That's still two more bytes than absolutely needed, and the \u003ccode\u003edebloated\u003c/code\u003e branch will definitely free up these wasted 640 bytes for portability reasons alone.\n\u003c/p\u003e\u003cp\u003e\n\tThis trail sprite cap of 48 seems a bit random at first. Unlike the regular bullet cap that the game enforces by just not spawning any new bullets if all 320 slots are occupied, the trail sprite cap is not enforced or even just checked in any way. Due to the circular nature of the buffer, the 49\u003csup\u003eth\u003c/sup\u003e simultaneously active bullet with a trail sprite will then \u003cq\u003eshare\u003c/q\u003e its position memory with the 1\u003csup\u003est\u003c/sup\u003e trail sprite bullet, leading to one additional position memory update per frame and trail sprites appearing in wrong positions.\u003cbr\u003e\n\tThus, it's the game design's responsibility to make minimal use of trail sprites to avoid these glitches. On the surface, it certainly \u003ci\u003elooks\u003c/i\u003e as if ZUN was careful here:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eThe cap happens to exactly match the 48 bullets fired as part of the ring group in Chiyuri's pattern, which is definitely the more bullet-intensive pattern of the two.\u003c/li\u003e\n\t\u003cli\u003eBoth trail-using patterns are part of Boss Attacks, and \u003cspan class=\"hovertext\" title='Launching a Boss Attack will always counter or \"reverse\" any active Boss Attack launched by the other player.'\u003eonly a single player's Boss Attack can be active at any given time\u003c/span\u003e.\u003c/li\u003e\n\t\u003cli\u003eThe ring groups in Chiyuri's pattern move fast and are separated by a 96-frame delay, as captured in the video above. By the time Chiyuri spawns the next group, every bullet of the previous one should have long been removed due to flying past the edges of the playfield.\u003c/li\u003e\n\t\u003cli\u003eMima fires her alternating 5- and 4-spreads at a much shorter interval, but that interval is still long enough to never leave more than 5 of these 9-bullet subpatterns on screen at once.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tUntil you test Chiyuri's pattern on the (\u003ca href=\"/blog/2025-05-10#bal-2025-05-10\"\u003e📝 announced\u003c/a\u003e) Boss Attack level 1 and notice that the bullets move slow enough for 2) to no longer apply. The result:\n\u003c/p\u003e\u003cfigure style=\"width: 320px\"\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.webp?6d45b071\" preload=\"none\" controls loop width=\"320\" height=\"384\" data-fps=\"56.423132\" data-frame-count=\"615\" style=\"aspect-ratio: 320 / 384\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.avi?27863503\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.webm?9cca61ba\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.webm?bfc7ca3d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.webm?5a03aba5\" type=\"video/webm\"\u003eVideo of TH03 Chiyuri firing multiple instances of her one Boss Attack pattern whose bullets use the trail sprite feature of the game's bullet system on Boss Attack level 1, demonstrating how the slower bullet speed will cause glitches when Chiyuri spawns a new 48-ring before all bullets of the previous 48-ring were clipped. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Chiyuri-trail-pattern-Level-1.avi?27863503\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"41\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"137\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"233\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"329\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"425\" data-title=\"5\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"521\" data-title=\"6\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tMissing bullets on every group beyond the first, and even sporadic trail sprites at mixed-up X and Y coordinates. This nicely demonstrates how these trail sprites are not \u003ci\u003ejust\u003c/i\u003e cosmetic, but also take control of clipping and affect gameplay as a result. For obvious optical reasons, trail sprite bullets will only get removed after the 6\u003csup\u003eth\u003c/sup\u003e remembered position lies outside of the clipping area – i.e., 6 frames later than bullets without trail sprites. If the remembered positions are then shared with a second bullet, the game would also clip \u003ci\u003ethat\u003c/i\u003e bullet if the clipping condition of the first one is met – regardless of the fact that the second bullet's main sprite might be nowhere close to the boundaries of the playfield. This clipping then either happens on the same frame if the second bullet's slot number within the 320-element bullet array is higher than the slot number of the first one, or on the next frame if the second bullet's slot number is lower.\u003cbr\u003e\n\tThe mixed-up X and Y positions on frames 139 and 235 can also be explained by clipping. The update function processes the X and Y coordinates independently from each other: It starts with the horizontal clipping checks, updates the position memory for the X coordinate, and then repeats both steps for the Y coordinate, immediately removing the bullet and moving on to the next one if it failed the respective clipping check. If the \u003ci\u003evertical\u003c/i\u003e clipping checks fail in a situation where two bullets share the same position memory, you'll end up with a mismatched X/Y pair where X comes from a clipped bullet and Y comes from an active one… for a single frame, until the same clipping check is applied to the other bullet and removes it as well. Hence, this is the only \"fixable\" bug in the bullet system that won't affect gameplay, as the mixed-up positions are unrelated to the result of the clipping condition that ultimately removes both bullets.\n\u003c/p\u003e\u003cp\u003e\n\tIt's rare for Chiyuri's Boss Attack to launch the same pattern multiple times in a row, and once the (announced) Boss Attack level is ≥3, bullets already move fast enough to prevent this quirk from happening. But it's definitely possible to run into it during regular gameplay.\n\u003c/p\u003e\u003cp\u003e\n\tFor a clearer and more extreme demonstration of the resulting glitches, let's turn Mima's trail sprite pattern into a 64-ring:\n\u003c/p\u003e\u003cfigure style=\"width: 288px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Mima-64-ring-trail.webp?b1e1f829\" preload=\"none\" controls loop width=\"288\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"56\" style=\"aspect-ratio: 288 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Mima-64-ring-trail.avi?4eeb4ee3\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Mima-64-ring-trail.webm?0f737adf\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Mima-64-ring-trail.webm?205e6cab\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Mima-64-ring-trail.webm?799ccde5\" type=\"video/webm\"\u003eVideo of TH03 Mima modded into firing a 64-ring of bullets that use the trail sprite feature of the game's bullet system, demonstrating how such a high number of bullets will cause trail sprite glitches even within individual ring groups. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Mima-64-ring-trail.avi?4eeb4ee3\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eExplaining every single quirk in this hypothetical video is left as an exercise to the reader.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003ch4 id=\"groups-2025-12-31\"\u003eRings and other groups\u003c/h4\u003e\u003cp\u003e\n\tIf there's one aspect where TH03's bullet system shows its TH02 lineage most clearly, it's the set of predefined bullet groups. The 2-, 3-, 4-, and 5-spreads with fixed narrow, medium, and wide arc angles, as well as the multi-bullet groups with randomized angles and speeds, are not only available in TH03 once again, but reuse the exact same code from TH02.\u003cbr\u003e\n\tInstead, TH03's main innovation can be found in its ring system. Rings can now have any number of bullets between 0 and 255, and are no longer limited to the first six powers of 2. This allowed ZUN to fine-tune most ring groups based on the Gauge or Boss Attack level, and to also just have a few static ring patterns with non-power-of-two bullet counts. Chiyuri's aforementioned 48-ring trail sprite pattern falls in this category, and the rotating 5-ring pattern seen in Kana's Boss Attack is another example.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 288px;\"\u003e\u003cimg\n\tsrc=\"/blog/static/2025-12-31-TH03-Kana-Boss-Attack-5-ring.webp?aae2f155\"\n\twidth=\"288\"\n\talt=\"Screenshot of the fixed 5-ring pattern used in TH03 Kana's Boss Attack\"\n\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd yes, storing the number of ring bullets in a regular \u003ccode\u003euint8_t\u003c/code\u003e field now also allows patterns to spawn 0-rings. And sure enough, the bug that would later cause \u003ca href=\"/blog/2022-04-18#kurumi-2022-04-18\"\u003e📝 Kurumi's \u003ccode\u003eDivide Error\u003c/code\u003e crash in TH04\u003c/a\u003e was actually introduced in TH03! The underlying code wasn't modified between the two games, which further proves that TH04's bullet system also traces back to TH03 and wasn't rewritten from scratch, at least concerning this aspect. TH03 just doesn't have any (known) way of triggering the bug in the unmodded original game.\u003cbr\u003e\n\tInterestingly, TH02's ring system with predefined power-of-2 bullet counts is still part of TH03, and ZUN does use it for some ring groups in a few Boss Attack patterns. Did he do this because it's shorter than adding a second line of code that sets \u003ccode\u003ebullet_template.count\u003c/code\u003e? Did he deliberately need to preserve the previous value of \u003ccode\u003ebullet_template.count\u003c/code\u003e across groups? Or did he code these patterns at an earlier time in development when the arbitrary ring system didn't exist yet? Until I've decompiled every single bullet pattern in this game, we can only guess.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, ZUN also removed two of TH02's group-related features from TH03:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tThe eight special motion types have been reduced to a single \u003ci\u003egravity\u003c/i\u003e type. While gravity is now a separate flag in the bullet structure and template that can now be applied to any group, this vast removal of options still severely limits the expressivity of bullet patterns in TH03. This means that every non-gravity bullet in the game moves at a constant velocity.\u003cbr\u003e\n\tGravity is also exclusively used by Kana, in both her Extra attack with the\n\t\u003cimg src=\"/blog/static/2025-12-31-TH03-bullet-5-Extra.webp?4054b4d1\" class=\"inline_sprite\" width=\"16\" alt=\"\"\u003e\n\talternative 16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e bullet sprites as well as in one certain pattern of her Boss Attack, which demonstrates gravity in combination with a ring group.\n\t\u003cfigure style=\"width: 288px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.webp?6946bdee\" preload=\"none\" controls data-title=\"Easy, Boss level 1\" loop data-active width=\"288\" height=\"384\" data-fps=\"56.423132\" data-frame-count=\"81\" style=\"aspect-ratio: 288 / 384\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.avi?d912af8e\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.webm?90d7cb87\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.webm?6dc0dca9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.webm?aa494edc\" type=\"video/webm\"\u003eVideo of TH03 Kana's one Boss Attack pattern whose bullets use the gravity feature of the game's bullet system, recorded on Easy on Boss Attack level 1 as a very satisying infinite loop. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Kana-gravity-pattern-Easy-level-1.avi?d912af8e\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.webp?a72711db\" preload=\"none\" controls data-title=\"Lunatic, Boss level 16\" loop width=\"288\" height=\"384\" data-fps=\"56.423132\" data-frame-count=\"81\" style=\"aspect-ratio: 288 / 384\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.avi?06776b1d\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.webm?4e07cb6a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.webm?a39e991d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.webm?683bf93a\" type=\"video/webm\"\u003eVideo of TH03 Kana's one Boss Attack pattern whose bullets use the gravity feature of the game's bullet system, recorded on Lunatic on Boss Attack level 16 as a very satisying infinite loop. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Kana-gravity-pattern-Lunatic-level-16.avi?06776b1d\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eOne of the rare patterns that arguably looks prettier on Easy, where the slower bullet speeds leave more room for the gravity effect to accelerate the fall.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe auto-stacking system was removed without any direct replacement. With TH03's more \u003ca href=\"/blog/2025-05-10#tuning-2025-05-10\"\u003e📝 numeric\u003c/a\u003e method of defining difficulty, ZUN no longer needed this quick mechanism to \u003ca href=\"/blog/2025-02-24#overview-2025-02-24\"\u003e📝 distinguish Easy and Normal from Hard and Lunatic\u003c/a\u003e. This was one of the better changes between the two games though; the auto-stacking system added \u003ca href=\"https://github.com/nmlgc/ReC98/blob/d892535e723b3691612363d1bbdbd2a54f43fb43/th02/main/bullet/bullet.hpp#L80-L84\"\u003ea quite annoying asterisk to the documentation of the random groups\u003c/a\u003e that is no longer needed in TH03.\u003cbr\u003e\n\tManually creating stacks is obviously still possible by spawning separate versions of the same group with gradually reduced speed. This \u003ci\u003emight\u003c/i\u003e be considered another practical advantage of the global bullet template, since you only need to mutate a single field before calling \u003ccode\u003ebullets_add()\u003c/code\u003e again. But really, nothing justifies global data.\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003ch4 id=\"transfer-2025-12-31\"\u003eTransferred pellets\u003c/h4\u003e\u003cp\u003e\n\tThis is the final gameplay feature that deserves its own section. Let's follow the pellet's X coordinate on its way from the spawn point to its destination on the other playfield:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eZUN calculates the destination coordinate on the target playfield as a random Q12.4 X coordinate between 0 and 288.\u003c/li\u003e\n\t\u003cli\u003eThis subpixel coordinate is translated to screen space, adding either 16 for the left playfield or 336 for the right one. ZUN does this using the regular pixel-space conversion function that is typically used to calculate blitting coordinates, losing subpixel precision in the process and forming a very minor quirk.\u003c/li\u003e\n\t\u003cli\u003eThe pellet's movement angle is calculated in screen space, aiming a screen-space version of the pellet's origin point at the coordinate from 2).\u003c/li\u003e\n\t\u003cli\u003eThe screen-space pixel coordinate from 2) is translated back to a Q12.4 subpixel coordinate on the pellet's \u003ci\u003eoriginating\u003c/i\u003e playfield. The result will deliberately lie outside the boundaries of this playfield: For a pellet flying from left to right, it will be between \u003cspan class=\"hovertext\" title=\"Playfield width (288) + center border width (32)\"\u003e320\u003c/span\u003e and 608, while it will be between -320 and -32 for a pellet flying from right to left.\u003c/li\u003e\n\t\u003cli\u003eThe pellet then flies to this out-of-bounds coordinate while internally staying on the playfield it originated on. This means that neither the update nor the rendering code can clip the pellet at the borders of its originating playfield. Once it flew past the border, it only visually appears on the other playfield because that's what the out-of-bounds X coordinate translates to when the renderer converts it to screen space.\u003c/li\u003e\n\t\u003cli\u003eOnce the pellet's X coordinate has approached or flown past this relative target coordinate from its respective movement direction, the pellet is removed and respawned as a delay cloud.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThis shows that the 32-pixel border between the two playfields is not just visual, but an actual part of the simulated game world. We can visualize this by removing the black cells on the text layer:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Pellet-transfer-original.webp?98f5ef32\" preload=\"none\" controls data-title=\"Original game\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"223\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Pellet-transfer-original.avi?1cefb881\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Pellet-transfer-original.webm?ba26287c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Pellet-transfer-original.webm?52ec7d40\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Pellet-transfer-original.webm?73cf90ad\" type=\"video/webm\"\u003eA typical video of typical transfer pellets in TH03, spawned by killing enemies with existing explosions. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Pellet-transfer-original.avi?1cefb881\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"148\" data-title=\"Last frame of transfer pellet aimed at \u003ccode\u003ecenter_y = 15.8125\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-12-31-TH03-Pellet-transfer-revealed.webp?5d2368d4\" preload=\"none\" controls data-title=\"Revealed pixels below playfield border\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"223\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-12-31-TH03-Pellet-transfer-revealed.avi?2c8c3a47\"\u003e\u003csource src=\"/blog/static/video/av1/2025-12-31-TH03-Pellet-transfer-revealed.webm?199e35e6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-12-31-TH03-Pellet-transfer-revealed.webm?d28d156a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-12-31-TH03-Pellet-transfer-revealed.webm?da1efda7\" type=\"video/webm\"\u003eVideo demonstrating how transferred pellets in TH03 have to physically travel through the 32 pixels before they appear on the other playfield, recorded by revealing the VRAM contents below the opaque black TRAM cells that hide these pixels in regular gameplay. \u003ca href=\"/blog/static/video/zmbv/2025-12-31-TH03-Pellet-transfer-revealed.avi?2c8c3a47\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"148\" data-title=\"Last frame of transfer pellet aimed at \u003ccode\u003ecenter_y = 15.8125\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eAlso, we need to clear these 32 border pixels in VRAM on every frame to nicely visualize \u003ci\u003ejust\u003c/i\u003e these pellet transfers. TH03 obviously doesn't do that for performance reasons and lets partially clipped sprites accumulate below the border, \u003ca href=\"/blog/2024-12-04#digits-2024-12-04\"\u003e📝 just like the other shmups do\u003c/a\u003e.\u003cbr\u003e\n\tThis video also demonstrates another minor quirk: Transferred pellets are aimed at a random center Y coordinate between 0 and 16, but the subsequent delay cloud is always spawned at \u003ccode\u003ecenter_y = 2.0\u003c/code\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"render-2025-12-31\"\u003e\u003cp\u003e\n\tSpeaking of, there's also a lot to cover in…\n\u003c/p\u003e\u003ch3\u003eTH03's bullet renderer\u003c/h3\u003e\u003cp\u003e\n\tAnd at first, it looks pretty good! TH03 retains the best idea from TH02 and batches rendering into three passes:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003e16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e bullets, rendered normally via SPRITE16\u003c/li\u003e\n\t\u003cli\u003e32×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e32\u003c/span\u003e delay clouds, rendered via SPRITE16's monochrome mode\u003c/li\u003e\n\t\u003cli\u003e8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e pellets, rendered from hardcoded sprites via the GRCG\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThe second pass is skipped if the first pass didn't detect at least a single active delay cloud. However, this skip can only remove 320 out of the 960 iterations over the entire bullet array, every frame. Combine that with the most unlucky allocation of registers, and the resulting instructions end up wasting a low 5-digit number of CPU cycles per frame on a 486 in the worst case of no bullets being active. Same game that wrote large parts of its bullet update function in ASM, by the way. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tWhile that number is still an order of magnitude away from causing significant performance problems, this issue became serious enough in TH04 for ZUN to introduce a display list for at least pellets that would cut down the number of iterations.\n\u003c/p\u003e\u003cp\u003e\n\tAnd then, we look at…\n\u003c/p\u003e\u003ch4 id=\"pellets-2025-12-31\"\u003ePellet rendering\u003c/h4\u003e\u003cp\u003e\n\t… and are greeted by the single strangest set of hardcoded sprites across all of PC-98 Touhou so far. TH03's pellets are not only the first time we see a \u003ci\u003edoubly-preshifted\u003c/i\u003e sprite sheet, but 2 of the 16 variants for the transfer pellet sprites are also shifted incorrectly:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 216px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\n\t\t\u003cdiv\u003e\u003c/div\u003e\n\t\t\u003cdiv\u003eYou can see this bug all over the video above, for example in frame 97, 105, 107, 109, 111…\u003c/div\u003e\n\t\u003c/figcaption\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-pellets.webp?9cd62d23\"\n\t\tdata-title=\"Sprite sheet\"\n\t\twidth=\"216\"\n\t\talt=\"TH03's pellet sprite sheet, with byte boundaries as red vertical lines\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-pellets-bug.webp?7a9d7428\"\n\t\tdata-title=\"Bug\"\n\t\twidth=\"216\"\n\t\talt=\"TH03's pellet sprite sheet, with byte boundaries as red vertical lines and the bugged transfer sprites at bit offsets 6 and 14 highlighted in red\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2020-10-06\"\u003e📝 Looks familiar?\u003c/a\u003e Now we know that ZUN still didn't have a tool for automatically preshifting sprites by the time he developed TH03 – something that \u003ca href=\"/blog/2020-07-09\"\u003e📝 I considered a necessity 5½ years ago\u003c/a\u003e. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tThe doubly-preshifted nature of this sprite sheet, on the other hand, raises a whole lot of PC-98 blitting performance questions. This only possibly makes sense as an attempt at optimizing away the unaligned 16-bit VRAM writes you'd naturally run into when shifting an 8-wide sprite to cover two bytes.\u003cbr\u003e\n\tLet's look at a regular 8-wide pellet sprite that was singly-preshifted to 16 pixels/bits. If we want to blit such a sprite to a left X position of 12 with the minimum amount of instructions, we would perform a single 2-byte write to VRAM address \u003ccode\u003e0x0001\u003c/code\u003e, which itself is not divisible by 2:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 576px;\"\u003e\u003cimg\n\tsrc=\"/blog/static/2025-12-31-Pellet-preshift-single-12.webp?66cad461\"\n\talt=\"Visualization of blitting a preshifted 8×8 pellet (16 pixels wide) to horizontal pixel #12, halfway within an odd byte address and crossing a 16-bit word boundary\"\n\twidth=\"576\"\n\u003e\u003c/figure\u003e\u003cp\u003e\n\tOn most 16-bit architectures, unaligned memory writes like these are either slower than aligned writes or entirely unsupported. The x86 \u003ccode\u003eMOV\u003c/code\u003e and \u003ccode\u003eMOVS\u003c/code\u003e instructions fall into the first category, so it makes sense to think that the GRCG might add a performance penalty of its own on top of the already higher latency of these instructions.\u003cbr\u003e\n\tThe natural workaround, then, is to add a second set of preshifted sprites to cover the remaining 8 possible start bit positions within a 16-pixel VRAM word. This would expand pellet sprites to a total width of 23 pixels. Understandably, ZUN also wanted to optimize for the low instruction counts, so he had to round up the physical width of the sprite to 32 pixels. Then, every preshifted variant could be blitted with a single \u003ccode\u003eMOVSD\u003c/code\u003e instruction:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 576px;\"\u003e\u003cimg\n\tsrc=\"/blog/static/2025-12-31-Pellet-preshift-double-12.webp?1fd43151\"\n\talt=\"Visualization of blitting a doubly-preshifted 8×8 pellet (32 pixels wide) to horizontal pixel #12, replacing the unaligned 16-bit word write with an aligned 32-bit write\"\n\twidth=\"576\"\n\u003e\u003c/figure\u003e\u003cp\u003e\n\tBut does this \u003ci\u003eactually\u003c/i\u003e matter for the PC-98 and the GRCG? Are unaligned writes \u003ci\u003eactually\u003c/i\u003e slow enough to justify writing 2× as much sprite data per frame and hardcoding 4× as many bytes? Unfortunately, I don't know of any hardware-level documentation about the GRCG that would conclusively answer this question. All the usual books and text files are disappointingly surface-level and only document the same programmer interfaces over and over, and hardware researchers are still waiting for EGC and GRCG die shots to even get started.\u003cbr\u003e\n\tThere are a few signs that this \u003ci\u003emight\u003c/i\u003e be a good idea:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThe GDC reads out VRAM in 16-bit words, as repeatedly stated on almost every page of \u003ca href=\"https://archive.org/details/bitsavers_necuPD7220ec85_3707077\"\u003eits hardware manual\u003c/a\u003e. This probably means the least though, because all \u003cq\u003e\u003cb\u003eG\u003c/b\u003eraphics \u003cb\u003eC\u003c/b\u003ehargers\u003c/q\u003e \u003ca href=\"https://archive.org/details/PC9801Bible/page/n11/mode/2up\"\u003esit at the other side of the dual-ported VRAM\u003c/a\u003e.\n\u003c/li\u003e\u003cli\u003e\n\tAny VRAM-reading EGC operation \u003ci\u003emust\u003c/i\u003e use aligned 16-bit accesses, which probably has a deeper reason that goes beyond the size of its internal shift register. And since you activate the EGC by first \u003ca href=\"https://github.com/nmlgc/ReC98/blob/3130b0ae7125bacb72bfac94ff8873315b87fefa/platform/x86real/pc98/egc.hpp#L59-L61\"\u003eactivating the GRCG in TDW mode\u003c/a\u003e…\n\u003c/li\u003e\u003cli\u003e\n\tNeko Project spends the same number of clock cycles on both \u003ca href=\"https://github.com/AZO234/NP2kai/blob/02b08deb3833305251fb3ee6c5d59b0efb5b52ff/mem/memvram.c#L40\"\u003e8\u003c/a\u003e- and \u003ca href=\"https://github.com/AZO234/NP2kai/blob/02b08deb3833305251fb3ee6c5d59b0efb5b52ff/mem/memvram.c#L66\"\u003e16-bit GRCG writes\u003c/a\u003e. The absence of a dedicated 32-bit write handler suggests that real hardware breaks down 32-bit writes into two 16-bit writes, implying that we don't also have to worry about \u003ci\u003e32-bit\u003c/i\u003e alignment of our single \u003ccode\u003eMOVSD\u003c/code\u003e instruction.\n\u003c/li\u003e\u003cli\u003e\n\tThe MAME developers \u003ca href=\"https://github.com/mamedev/mame/commit/0e4ba6d49ad68d13966dc2cd14adbe68a7b3b684\"\u003eremoved the 8-bit GRCG write function during a massive refactor 4 years ago\u003c/a\u003e, without explaining why. The MAME-typical high-level hardware explanation is still missing as well.\n\u003c/li\u003e\u003cli\u003e\n\tThe \u003ccite\u003ePC-9800Series Technical Data Book Hardware\u003c/cite\u003e contains this little note in \u003ca href=\"https://archive.org/details/PC9800TechnicalDataBookHARDWARE1993/page/n205/mode/2up\"\u003eits description of the GRCG's RMW mode on page 192\u003c/a\u003e:\n\t\t\u003cblockquote lang=\"ja\" class=\"hovertext\" title=\"Byte-accessible\"\u003eバイトアクセス可能\u003c/blockquote\u003e\n\tShouldn't byte access be a given? Clearly, this would only deserve special mention if it \u003ci\u003ewasn't\u003c/i\u003e because the previous contents of this book heavily implied some sort of 16-bit nature and I just missed it.\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tBut without documentation or benchmarks, none of this means anything.\u003cbr\u003e\n\tThis is also why I haven't yet explored this whole field of optimizing VRAM writes for alignment. It would always involve branching to alignment-respecting code \u003ca href=\"https://github.com/nmlgc/ReC98/blob/3130b0ae7125bacb72bfac94ff8873315b87fefa/libs/master.lib/super_put.asm#L79-L85\"\u003esimilar to how master.lib does it\u003c/a\u003e, but code like this is at odds with the more tangible goal of minimizing instruction counts in the generic case. Not to mention that we'll once again have to test this across every PC-98 hardware generation and possibly even GRCG revision if we ever go down to \u003ci\u003ethat\u003c/i\u003e level of optimization…\n\u003c/p\u003e\u003cp\u003e\n\tBut even \u003ci\u003eif\u003c/i\u003e alignment matters, ZUN's unconditional \u003ccode\u003eMOVSD\u003c/code\u003e instructions approach still appears to be slower on average. Consider the optimal 56.25% of cases where the sprite \u003ci\u003edoes\u003c/i\u003e lie within a single 16-bit word:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 576px;\"\u003e\u003crec98-child-switcher\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-Pellet-preshift-double-0.webp?5a41677e\"\n\t\talt=\"Visualization of blitting a doubly-preshifted 8×8 pellet (32 pixels wide) to horizontal pixel #0, demonstrating how doubly-preshifted sprites wastefully write the second 16-bit word in the optimal 56.25% of cases\"\n\t\twidth=\"576\"\n\t\tdata-title=\"Leftmost best case (\u003ccode\u003e(x % 16) == 0\u003c/code\u003e)\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-Pellet-preshift-double-8.webp?b54dccad\"\n\t\talt=\"Visualization of blitting a doubly-preshifted 8×8 pellet (32 pixels wide) to horizontal pixel #8, demonstrating how doubly-preshifted sprites wastefully write the second 16-bit word in the optimal 56.25% of cases\"\n\t\twidth=\"576\"\n\t\tdata-title=\"Rightmost best case (\u003ccode\u003e(x % 16) == 8\u003c/code\u003e)\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\t8 start positions within the first byte + 1 start position on the second byte\u0026nbsp;= \u003csup\u003e9\u003c/sup\u003e/\u003csub\u003e16\u003c/sub\u003e\u0026nbsp;= 56.25%. The 9\u003csup\u003eth\u003c/sup\u003e variant for \u003ccode\u003e(x\u0026nbsp;%\u0026nbsp;16)\u0026nbsp;== 8\u003c/code\u003e wouldn't be part of a regular singly-preshifted sprite sheet where the renderer blits the \u003ccode\u003e(x\u0026nbsp;%\u0026nbsp;8)\u003c/code\u003e\u003csup\u003eth\u003c/sup\u003e\u0026nbsp;= 0\u003csup\u003eth\u003c/sup\u003e variant. But it would definitely be worth adding if alignment does matter at all.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tKeep in mind that we still use the GRCG here, and that it will also have to perform its fast-but-not-entirely-free four-plane Read-Modify-Write operation for the empty sprite bytes 3 and 4. Unconditional 32-bit writes would only be worth it if the GRCG somehow optimizes away empty writes at the microarchitecture level. That assumption is even more of a stretch, because \u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 why would master.lib even check for emptiness\u003c/a\u003e if that were true?\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, doubly-preshifted sprites slow down 56.25% of all blitting operations in a dubious attempt to speed up the other 43.75%. Unaligned 16-bit writes would have to be \u003ci\u003ereally\u003c/i\u003e slow to justify this approach – and judging from the fact that TH04 went back to single-byte preshifting, this is not the case. Maybe I'll write a benchmark for this someday, but honestly, this is the least interesting PC-98 benchmark question I've encountered so far. There \u003ci\u003eare\u003c/i\u003e slowdown issues at \u003ca href=\"/blog/2025-09-06#66-2025-09-06\"\u003e📝 our performance target of 66\u0026nbsp;MHz in Neko Project\u003c/a\u003e, but pellet sprite alignment is unlikely to significantly contribute to those.\n\u003c/p\u003e\u003chr id=\"clouds-2025-12-31\"\u003e\u003cp\u003e\n\t16×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e16\u003c/span\u003e bullets are simply rendered using standard SPRITE16 calls, nothing special there. That only leaves…\n\u003c/p\u003e\u003ch4\u003ePellet delay clouds\u003c/h4\u003e\u003cfigure class=\"pixelated checkerboard\" style=\"width: 128px;\"\u003e\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-pellet-cloud-0.webp?fbda9b15\"\n\t\talt=\"Animation frame #1 of TH03's pellet delay clouds\"\n\t\twidth=\"128\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-pellet-cloud-1.webp?770f553a\"\n\t\talt=\"Animation frame #2 of TH03's pellet delay clouds\"\n\t\twidth=\"128\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-pellet-cloud-2.webp?278c62c1\"\n\t\talt=\"Animation frame #3 of TH03's pellet delay clouds\"\n\t\twidth=\"128\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003c/figure\u003e\u003cp\u003e\n\tJust like the 48×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e48\u003c/span\u003e \u003ca href=\"/blog/2024-04-24#hitcirc-2024-04-24\"\u003e📝 hit circle\u003c/a\u003e, these 32×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e32\u003c/span\u003e sprites are rendered using SPRITE16's single-color render-path, which uses the EGC's GRCG-equivalent mode. Last year, I took a very brief look at this mode and wondered whether this was actually faster than just using the GRCG. 1½ years and \u003ca href=\"/blog/2025-09-10\"\u003e📝 one benchmark won by the EGC\u003c/a\u003e later, it certainly seems so, especially since we want to blit these to unaligned X positions. The EGC's hardware-accelerated pixel shifting seems highly preferable once sprite widths exceed 24 pixels and you can't fit a row of pixels in a 32-bit register anymore.\u003cbr\u003e\n\tStepping through SPRITE16 reveals that this GRCG-equivalent mode matches the GRCG even in how it doesn't read monochrome sprite data from VRAM, but from SPRITE16's 1bpp alpha mask buffer in conventional RAM.\n\u003c/p\u003e\u003cp\u003e\n\tBut that only raises the question of why you'd want to use SPRITE16 over the raw EGC. It makes sense why SPRITE16 would \u003ci\u003ehave\u003c/i\u003e this feature; flashing existing sprites in a single color every once in a while is a useful thing to have in a game-focused rendering API. But using this feature for sprites that are \u003ci\u003eonly\u003c/i\u003e rendered in this monochrome mode just wastes the VRAM that these sprites occupy in SPRITE16's sprite area. You still blit such a sprite by passing a byte offset into the sprite area, which then gets interpreted as an offset into SPRITE16's alpha mask buffer.\u003cbr\u003e\n\tIf SPRITE16 had a function for directly blitting from a pointer to 1bpp data, ZUN could have freed up quite a bit of VRAM and maybe even added more sprites for character-specific attacks. Conceptually, it makes sense why SPRITE16 would restrict itself to a single sprite source, but it is quite an unfortunate omission, I'd say.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 640px\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-TH03-SPRITE16-area-monochrome.webp?53619a67\"\n\t\twidth=\"640\"\n\t\talt=\"TH03's SPRITE16 sprite area, with monochrome sprites highlighted\"\n\t\u003e\n\t\u003cfigcaption\u003e\n\t\t13,568 pixels, to be exact. And yeah, you could \u003ci\u003etechnically\u003c/i\u003e overwrite the affected portions of VRAM after generating alpha masks via \u003ccode\u003eINT 42h, AH=01h\u003c/code\u003e. But since SPRITE16 only stores one such alpha mask buffer, you still couldn't reuse this space for other SPRITE16 sprites.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd that was the last PC-98 Touhou bullet system we were still missing! But at a little over 2 pushes, I have to find something else to do to round out the third one… wait, what about that one incident?\n\u003c/p\u003e\u003chr\u003e\u003ch3 id=\"fedi-2025-12-31\"\u003eMigrating away from Twitter\u003c/h3\u003e\u003cp\u003e\n\tOn \u003ca href=\"https://twistedvoxel.com/massive-ban-wave-targets-oldtweetdeck-users-following-xs-crackdown-on-third-party-access/\"\u003eNovember 6\u003c/a\u003e, Twitter was hit by an automated ban wave that suspended all accounts that were using the \u003ca href=\"https://github.com/dimdenGD/OldTweetDeck\"\u003eOldTweetDeck extension\u003c/a\u003e. After Twitter discontinued the official free TweetDeck frontend on 2023-08-17, I quickly switched to OldTweetDeck – not just because it was free, but because it supported multiple accounts and was simply more performant than X's own premium offering at the time. In return, I gladly paid my €10 a month to dimden instead, who deserved it much more for continuously updating OldTweetDeck to all of Twitter's API changes over the years. It's very impressive how he kept it running for 2⅓ years without any such critical issues and still keeps maintaining it to this day.\u003cbr\u003e\n\tAside from \u003ca href=\"https://www.facebook.com/thpatch/posts/pfbid0xM3n3SYBdsM7WQbEE2TjSs3RDiDq98iKfNRwoj1E58nG9YRKe2gks1tmNo447Re5l\"\u003eTouhou Patch Center\u003c/a\u003e and all of my accounts, the ban wave affected \u003ca href=\"https://github.com/dimdenGD/OldTweetDeck/issues/459\"\u003eenough people\u003c/a\u003e that Twitter decided to gradually revert it a day later. But without any public postmortem or excuse, this feels more like an act of gratitude that we shouldn't take for granted.\n\u003c/p\u003e\u003cp id=\"downfall-2025-12-31\"\u003e\n\tEver since Elon took over, the Internet has been full of sensationalist doomposts about Twitter's \u003cq\u003eimminent downfall any moment now OMG\u003c/q\u003e. For the longest time, I could ignore all these pundits because nothing of what they were complaining about was affecting my little corner. But sudden account suspensions are an existential threat to my business, and finally provided the first actual technical and non-political argument to get my data off Twitter in the medium term. I've put too much effort into all of the content there to let it be exclusively controlled by any one company.\n\u003c/p\u003e\u003cp\u003e\n\tHilariously, things only got worse from there. Until two days ago, \u003ca href=\"https://www.reddit.com/r/twitterhelp/comments/1pn1ul1/xtwitter_data_download_glitch/\"\u003eTwitter's data download option was inaccessible due to an infinite redirection bug\u003c/a\u003e. Call it malice or incompetence, but leaving such an issue unfixed for \u003ci\u003eweeks\u003c/i\u003e is a definite sign of a platform in decline. Thus, I had to run the import on an older archive I happened to request on 2023-07-02.\u003cbr\u003e\n\tAnd then I looked \u003ci\u003einside\u003c/i\u003e that archive and noticed that it was missing at least three key pieces of data that Twitter demonstrably stores for my account:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003ePoll options\u003c/li\u003e\n\t\u003cli\u003eAlt text for images. (Also known as the actually most annoying and time-consuming part of every tweet if you actually want to properly \u003ci\u003eexplain\u003c/i\u003e an image with all its context and implications. AI won't help with that as long as its context window doesn't span every piece of knowledge related to this project. 🤷)\u003c/li\u003e\n\t\u003cli\u003eThe original version of each uploaded image, which they do have for a fact because it's shown in the \u003ccode\u003e/status\u003c/code\u003e view. The archive only contains the \u003cq\u003eprocessed\u003c/q\u003e versions shown in the timeline, which were resized to at most 1200 pixels along their larger dimension and which may or may not have been converted to JPEG based on rules I didn't bother to reverse-engineer.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThat's a rather selective interpretation of \u003ca href=\"https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679\u0026from=EN#art_20\"\u003eArt. 20 GDPR\u003c/a\u003e. If the argument is that \u003cq\u003eyou can just scrape that data out of the HTML yourself\u003c/q\u003e, why are they even bothering with sending me anything more than a nested list of tweet IDs, then? 🤨 Someone with more time and care could probably turn this into a lawsuit…\u003cbr\u003e\n\tPresenting all media in its original quality is one of the more important reasons for moving to a self-hosted service as far as I'm concerned, especially since mainstream media conversion pipelines are infamous for destroying pixel art. So I went through my hard drives and replaced Twitter's images with the original versions of all 167 non-retweeted images I had uploaded to Twitter until July 2023. The videos also desperately needed to be replaced with their original AV1 versions; Twitter's enforced x264 YUV420P format has been the single worst implementation detail of that entire platform…\n\u003c/p\u003e\u003ch4 id=\"bluesky-2025-12-31\"\u003eTrying a Bluesky PDS\u003c/h4\u003e\u003cp\u003e\n\tSo, time to complete the \u003ca href=\"/blog/2024-11-22#website-2024-11-22\"\u003e📝 Bluesky self-hosting plan from last year\u003c/a\u003e? Setting up a PDS isn't all too annoying, but the import process leaves a lot to be desired:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003e\n\t\tYou \u003ci\u003ecan\u003c/i\u003e backdate posts by modifying their \u003ci\u003ecreation\u003c/i\u003e time, but Bluesky's crawlers will also record the \u003ci\u003eindexed\u003c/i\u003e time when they first saw each post on the network. Unfortunately, the \u003ca href=\"https://bsky.app\"\u003ebsky.app\u003c/a\u003e frontend that everyone uses will then present this indexed time as a post's main timestamp, demoting your intended creation time to an \u003cq\u003earchival time\u003c/q\u003e that Bluesky \u003cq\u003ecan't confirm the authenticity of\u003c/q\u003e:\n\t\u003c/p\u003e\n\t\u003cfigure\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-12-31-Bluesky-crawl-override.webp?4ee5c75c\"\n\t\talt=\"Screenshot of a certain microblogging platform's insistence to prominently display the time it crawled a post from a PDS, demoting the PDS's actual post creation time to an \u0026quot;archived\u0026quot; time.\"\n\t\u003e\u003c/figure\u003e\n\t\u003cp\u003e\n\t\tThe PDS database schema does track an \u003ccode\u003eindexedAt\u003c/code\u003e timestamp in addition to the \u003ccode\u003ecreatedAt\u003c/code\u003e timestamp you specify during the import, but \u003ccode\u003eindexedAt\u003c/code\u003e might as well not exist because it doesn't seem to be used anywhere.\u003cbr\u003e\n\t\tThere is \u003ca href=\"https://github.com/bluesky-social/social-app/pull/7466\"\u003ea PR that would slightly improve the UI in this case\u003c/a\u003e, but it's been languishing unmerged throughout most of 2025. Probably because it has to be merged by \u003ca href=\"https://news.ycombinator.com/item?id=43401855\"\u003ethe same people who came up with the current UI in the first place\u003c/a\u003e, and who prioritized resilience against pranks and disinformation campaigns.\u003cbr\u003e\n\t\tBut even \u003ci\u003eif\u003c/i\u003e the UI is fixed, these imports would spam the timeline of everyone who follows the existing Bluesky account that we obviously want to import into.\n\t\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tWhy does \u003ca href=\"https://tangled.org/marcomaroni.it/twitter-to-bluesky/blob/7bc20067408887fce95af50379f5ea47a7ddbe14/app.ts#L870-874\"\u003ethe best-known importing script  convert any non-JPEG image to JPEG\u003c/a\u003e? 🤨 Removing the conversion and just uploading the PNG files from Twitter seems to cause any tweet with a post to simply not get imported at all. Of course, \u003ca href=\"/blog/2025-04-09#webp-2025-04-09\"\u003e📝 as we know since April\u003c/a\u003e, lossless WebP is preferable over PNG anyway, but, uh, \u003ca href=\"https://github.com/bluesky-social/social-app/issues/7629\"\u003ewhat the hell, are they serious\u003c/a\u003e?!\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tFiguring out and confirming that first issue required remote debugging of the PDS server written in Node.js. Visual Studio Code's LSP quickly ran up against my server's low amount of RAM, which forced me to upgrade my server just to efficiently navigate through the source code…\u003cbr\u003e\n\tTypical Node.js criticisms aside, the architecture of the PDS server is quite bizarre. A whole lot of the apparent API surface is never directly called, but generically proxied to some other node in the AT Protocol network at the byte level. If you log into your PDS via \u003ca href=\"https://bsky.app\"\u003ebsky.app\u003c/a\u003e, it \u003ci\u003eseems\u003c/i\u003e as if the AppView calls API endpoints like \u003ccode\u003e/xrpc/app.bsky.unspecced.getPostThreadV2\u003c/code\u003e on your PDS, but good luck meaningfully intercepting any of these requests, or even just getting your debugger to break on them.\u003cbr\u003e\n\tTogether with lots of bulky API schema descriptions in the form of \u003cq\u003e\u003ca href=\"https://atproto.com/guides/lexicon\"\u003elexicons\u003c/a\u003e\u003c/q\u003e, all this XRPC code makes up a big proportion of the code in the \u003ca href=\"https://www.npmjs.com/package/@atproto/pds\"\u003e\u003ccode\u003e@atproto/pds\u003c/code\u003e package\u003c/a\u003e. But for… what exactly? Why would the PDS server need a thick layer of type safety and validation for payloads it doesn't look at, and that the relays will have to verify \u003ci\u003eanyway\u003c/i\u003e? Why do they install all this dead code that will confuse most people who are trying to understand this system? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIn the end, we just can't thoroughly backdate our imported posts because the crawl timestamps are set by the relays, whose code we have no control over. Now, I could ignore all these issues and still upload \u003ci\u003esome\u003c/i\u003e sort of full archive to the platform that now houses \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e6\u003c/sub\u003e of my following, but this just doesn't match the quality I expect from the canonical, definitive source of my short-form news posts.\n\u003c/ul\u003e\u003ch4 id=\"mastodon-2025-12-31\"\u003eTrying Mastodon\u003c/h4\u003e\u003cp\u003e\n\tThat leaves the Fediverse as the only remaining alternative for a service where people can still follow, like, and repost my content using relatively commonly used clients. Among the various ActivityPub implementations, \u003ca href=\"https://misskey-hub.net/\"\u003eMisskey\u003c/a\u003e is particularly popular among the Japanese Touhou community, but I've only heard bad things about its resource usage. Mastodon isn't the most lightweight option either – as aptly implied by its name – but you can make the argument that it's become the default option across the Fediverse over the years. Thus, there'll be at least a slight chance that people will be familiar with the web UI of what I'm about to self-host.\n\u003c/p\u003e\u003cp\u003e\n\tToo bad that I didn't even get through the \u003ca href=\"https://docs.joinmastodon.org/admin/install/\"\u003efirst page of the setup guide\u003c/a\u003e before being stumped by obscure asset precompilation errors that apparently no one else has ever faced. In a way, it's commendable that a project would exclusively explain a bare-metal from-source setup in the Docker-dominated DevOps seascape of 2025. But why would you want to do this for a project that requires servers to be infested with npm \u003ci\u003eand\u003c/i\u003e Postgres \u003ci\u003eand\u003c/i\u003e a bleeding-edge self-compiled version of Ruby \u003ci\u003eand\u003c/i\u003e several \u003ccode\u003e-dev\u003c/code\u003e packages for C dependencies of certain Ruby gems? Unsurprisingly, Japanese Python behaves just like Dutch Ruby in how the community effectively treats every minor version as a major version because there are no adults left in the room to put all the \u003ca href=\"https://www.youtube.com/watch?v=YnL9vAFphmE\"\u003echildren and Ph.D\u003c/a\u003es in their place…\n\u003c/p\u003e\u003cp\u003e\n\tFortunately, ActivityPub is relatively simple to implement and there are plenty of existing servers that are better suited to the kind of PR channel I'm actually looking for. After a very quick search, I settled on…\n\u003c/p\u003e\u003ch4 id=\"gotosocial-2025-12-31\"\u003eGoToSocial\u003c/h4\u003e\u003cp\u003e\n\t…which immediately impresses in pretty much every single area:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAn \u003ca href=\"https://docs.gotosocial.org/en/v0.20.2/user_guide/importing_posts/#for-developers\"\u003eexplicit backdating feature for imports\u003c/a\u003e? In an API that's compatible with Mastodon's and requires minimal adjustments to existing Mastodon import scripts, instead of requiring that feature \u003ca href=\"https://github.com/KevinPayravi/twitter-archive-to-mastodon/tree/36ad7fb4849e02daff470fbcebceed71344650fb?tab=readme-ov-file#mod-mastodon\"\u003eto be temporarily modded into the server\u003c/a\u003e?\u003c/li\u003e\n\t\u003cli\u003eAfter the two previous bloatfests, it's very refreshing to see a single binary next to a bunch of static assets. Sure, 87.4\u0026nbsp;MiB is certainly way more bulky than necessary, but still much smaller than either of our two competitors.\u003c/li\u003e\n\t\u003cli\u003eThe documentation is \u003ci\u003eextremely\u003c/i\u003e well-organized and polished, especially for a project that's on version 0.20.2.\u003c/li\u003e\n\t\u003cli\u003eI can write Markdown in posts!\u003c/li\u003e\n\t\u003cli\u003eWebP and AV1? Just work too, without any attempt to convert the main image or video that gets attached to a post. Sure, the thumbnailer does convert images, but that's way less critical…\u003c/li\u003e\n\t\u003cli\u003e…and you can effectively bypass it by passing some five-digit size to \u003ccode\u003emedia-thumb-max-pixels\u003c/code\u003e. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eThe whole thing works exactly like a lightweight server \u003ci\u003eshould\u003c/i\u003e work: A single binary serving posts from a SQLite database and media attachments from static files lying next to it. With easy access to every piece of data, fixing typos and import errors after the fact is trivial. Applying these might need a server restart for caching reasons, but they're immediately reflected in whatever app is accessing the data.\u003c/li\u003e\n\t\u003cli\u003eDrawbacks? The database schema is highly redundant, poster image conversion for videos results in weirdly green images for every one of my AV1 source files, and the paginated timeline view could use just a \u003ci\u003efew\u003c/i\u003e more navigation options and customizability. Other seemingly missing features like posting and search are handled by third-party clients like the very admirable \u003ca href=\"https://pinafore.social/\"\u003ePinafore\u003c/a\u003e. And except for the first issue, these are all relatively minor, and I might even fix them myself one day. \u003ci\u003eThat's\u003c/i\u003e how you get new contributors to your free software project.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd just in case \u003ci\u003eyou\u003c/i\u003e ever want to import a Twitter archive onto a GoToSocial instance, here is the no-nonsense importer I used:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/twitter-archive-to-gotosocial\"\u003e\n\ttwitter-archive-to-gotosocial\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo if you've got an account on Misskey, Mastodon, or another ActivityPub server, please follow \u003ca href=\"https://activitypub.nmlgc.net/@rec98\"\u003e@rec98@nmlgc.net\u003c/a\u003e. I'll keep posting everything to both Twitter and Bluesky for the time being, but will no longer advertise either of them. If they ever go down, I'll make no attempt at restoring them.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that was 2025! It surely brought lots of words, breaking even last year's record by an additional 37% of blog post content. 😮 Here's to 2026 bringing more of the \u003ci\u003eactual\u003c/i\u003e reverse-engineering we've been sorely lacking with all the modding and porting projects over the past few years. And with at least four TH03 gameplay pushes queued up, things are already looking quite promising…\u003cbr\u003e\n\tNext up: Enemies! Formation scripts! Fireballs! Explosions! Combos, or at least the first part of them! And a slightly more common glitch that players have been wondering about for many years…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003cdiv title=\"Already at the latest blog post.\"\u003e🔼\u003c/div\u003e\u003ca href=\"#2025-10-19\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-12-31T22:43:39Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-10-19",
      "url": "https://rec98.nmlgc.net/blog/2025-10-19",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-12-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-10-19\"\u003e\u003ctime datetime=\"2025-10-19T21:52:40Z\"\u003e2025-10-19 21:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0324\"\u003eP0324\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (SDL 3 Windows 98 backport + Screenshot compression round-up)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/e42726f...d9c0a9f\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/SDL/compare/c016315...P0324\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/tupblocks/compare/142e011...12877be\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0325\"\u003eP0325\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (GCC ≥15 support / Resilient API configuration) + TH02-TH05 debloating round-up\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/d9c0a9f...a46a094\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0326\"\u003eP0326\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (SDL 3 in frontend, part 1 + SDL 3 file I/O + Screenshot bugfixes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/a46a094...P0326\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eRoot, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\u003cstyle\u003e\n\t.bmp-2025-10-19 thead th {\n\t\ttext-align: center;\n\t}\n\t.bmp-2025-10-19 td:not(:last-child),\n\t.bmp-2025-10-19 th:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\t.bmp-2025-10-19 tbody tr:nth-child(3) td,\n\t.bmp-2025-10-19 tbody tr:nth-child(3) th {\n\t\tborder-bottom: var(--table-border);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tSo, one push to make up for \u003ca href=\"/blog/2025-04-09\"\u003e📝 Shuusou Gyoku screenshots being closer to 4 pushes than 3\u003c/a\u003e, a second push to make up for \u003ca href=\"/blog/2025-09-06\"\u003e📝 the big PC-98 Touhou portability subproject being closer to 12 pushes than 11\u003c/a\u003e… and a third push because the planned Shuusou Gyoku maintenance turned out to actually involve significant work? I did not expect that implementing my vision would involve \u003ca href=\"https://github.com/libsdl-org/SDL/pulls?q=author%3Anmlgc+created%3A2025-10-01..2025-10-16\"\u003esending four pull requests to SDL\u003c/a\u003e that fixed three bugs and added one small feature.\u003cbr\u003e\n\tOn the flipside, it's great to see how my contributions were reasonable and well-explained enough for Sam Lantinga to merge them pretty much instantly. It's things like these, the \u003ca href=\"https://github.com/libsdl-org/SDL/pull/14210\"\u003emerged support for ancient MSVC versions\u003c/a\u003e, or the \u003ca href=\"https://github.com/libsdl-org/SDL/pull/13906\"\u003eongoing DOS port that will probably be merged as well\u003c/a\u003e, that give SDL a sense of being more of a community-owned project as opposed to a more tightly controlled one. We should definitely try upstreaming our Windows 98 port too, once it's done.\n\u003c/p\u003e\u003cp\u003e\n\tMost of the changes in this build concern aspects I've explained at length in earlier blog posts:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2025-01-25#modules-2025-01-25\"\u003e📝 As planned in January\u003c/a\u003e, the Linux build now compiles on GCC ≥15! As usual for C++ compilers, this switch once again required a nonzero amount of changes to make this codebase compile without errors or warnings, but that set of changes was far smaller this time around than it was when I added Clang support back in December.\u003cbr\u003e\n\tSeeing how GCC lacks support and certain overloads for \u003ca href=\"/blog/2025-01-25#porting-2025-01-25\"\u003e📝 a different set of C++ range algorithms\u003c/a\u003e is so tragic that it's almost hilarious again at this point. As for actual annoyances though, GCC still struggles with \u003ca href=\"/blog/2024-10-22#modules-2024-10-22\"\u003e📝 import-then-\u003ccode\u003e#include\u003c/code\u003e\u003c/a\u003e, the apparent prime challenge of implementing C++ modules that both MSVC and Clang have largely solved by now. The resulting redefinition errors pretty much force us to move all \u003ccode\u003e#include\u003c/code\u003es of third-party C library headers to above the first \u003ccode\u003eimport\u003c/code\u003e. In \u003ccode\u003e.cpp\u003c/code\u003e files, this is no problem, but what if we need any of those third-party data types in our headers? After all, we can \u003ccode\u003e#include\u003c/code\u003e our headers in any order and can thus no longer guarantee that the third-party \u003ccode\u003e#include\u003c/code\u003es will come before the \u003ccode\u003eimport std;\u003c/code\u003e statement we need in our headers. Further C++-modularization of our logic code is way beyond the scope of these three pushes, so we have no choice but to completely remove any third-party header \u003ccode\u003e#include\u003c/code\u003es from our headers. If we need their declarations, we now have to resort to \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/game/graphics.h#L127\"\u003epre-declared struct types\u003c/a\u003e and \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/game/format_bmp.h#L68-L70C2\"\u003eeven worse \u003ccode\u003e#ifdef\u003c/code\u003e hacks for enum types\u003c/a\u003e… Oh well, this \u003ci\u003edoes\u003c/i\u003e speed up the build ever so slightly in the end. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tOn a more positive note, GCC brought another set of highly useful warnings to the table, especially in conjunction with the \u003ccode\u003eCFLAGS\u003c/code\u003e that Arch Linux sets by default for every package built with \u003ccode\u003emakepkg\u003c/code\u003e. Special shoutout to \u003ccode\u003e-Wformat-overflow\u003c/code\u003e, which \u003ca href=\"https://gitlab.archlinux.org/archlinux/rfcs/-/blob/f196c4e5fd4c4645adff35d0431f06dbc03dd177/rfcs/0003-buildflags.md#specification\"\u003eArch Linux specifies via \u003ccode\u003e-Werror=format-security\u003c/code\u003e\u003c/a\u003e. This warning brings \u003ccode\u003esprintf()\u003c/code\u003e buffer size validation to its logical conclusion: It doesn't just consider the format string and arguments in isolation, but factors in \u003ci\u003eall\u003c/i\u003e statically available information and even control flow to precisely determine the \u003ci\u003eexact\u003c/i\u003e required size of the output buffer, and then warns if the given buffer is too small.\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003evoid format_stage_number(uint8_t stage)\n{\n\t// This is a classic shmup, we only ever have 6 stages, so 1 digit\n\t// plus terminating 0 is enough, right?\n\tchar buffer[1 + 1];\n\n\t// Wrong: A `uint8_t` can range from 0 to 255 inclusive, and the\n\t// compiler can't statically prove that [stage] will only ever have\n\t// a single-digit value. By comparing the (statically known) value\n\t// range of [stage] to the (statically known) [buffer] size, GCC's\n\t// `-Wformat-overflow` can then precisely warn that [buffer] must\n\t// be at least four characters large to safely avoid buffer\n\t// overflows in all possible circumstances.\n\tsprintf(buffer, \"%d\", stage);\n\n\t// This, on the other hand, causes no warning because the string\n\t// can't possibly take up more than two bytes.\n\tconst uint8_t known_stage = 1;\n\tsprintf(buffer, \"%d\", known_stage);\n\n\t// This also passes with no warning! Compilers are awesome.\n\tif(stage \u003e= 10) {\n\t\treturn;\n\t}\n\tsprintf(buffer, \"%d\", stage);\n}\n\u003c/pre\u003e\u003cfigcaption\u003e\n\t\u003ca href=\"https://godbolt.org/z/hjzT661Go\"\u003eHere's the full warning.\u003c/a\u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tNow that Shuusou Gyoku can compile with both GCC and Clang, it makes sense to support both of them in the build system without requiring users to edit the Lua files. Defining a generic *nix toolchain that just uses the \u003ccode\u003ecc\u003c/code\u003e and \u003ccode\u003ec++\u003c/code\u003e symlinks might look like a good idea due to the general compatibility of GCC's and Clang's command-line flags, but wouldn't work for us due to \u003ca href=\"/blog/2025-01-25#modules-2025-01-25\"\u003e📝 the completely different command-line flags needed for C++ modules\u003c/a\u003e. The build system must know in advance what's behind these symlinks in order to generate the correct set of build rules for Tup.\u003cbr\u003e\n\tWhile CMake solves this compiler detection issue by \u003ca href=\"https://gitlab.kitware.com/cmake/cmake/-/blob/c7089d67517272e8a4932986f434f603b541b0e6/Modules/CMakeDetermineCompilerId.cmake\"\u003ecompiling a test program that accesses a compiler's predefined macros\u003c/a\u003e, I went for the much simpler solution of parsing the string returned by \u003ccode\u003ecc --version\u003c/code\u003e. If you want to use a different compiler after all, you can always override \u003ccode\u003ecc\u003c/code\u003e with the \u003ccode\u003eCC\u003c/code\u003e environment variable, as you'd expect.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2025-04-09#sdl3-2025-04-09\"\u003e📝 As planned in April\u003c/a\u003e, the config file now stores the selected graphics API in terms of SDL's driver identifier string rather than using the more volatile index into SDL's build-specific driver list.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2025-04-25#sdl-2025-04-25\"\u003e📝 As also planned later in April\u003c/a\u003e, I now went all in on SDL 3, made it a hard dependency, and removed the SDL 2 code path, which paved the way for a much simpler and more featureful overall architecture. And yes, this means that even the Windows 98 build runs on SDL 3 now! The Windows 98 port of SDL 3 reuses most of the small changes we needed for SDL 2, but required \u003ca href=\"https://github.com/nmlgc/SDL/compare/c016315ec769da0eacd4bdb31bab9fa925fde614...P0324\"\u003ea few more on top\u003c/a\u003e to compile SDL 3's expanded feature set without warnings.\n\u003c/p\u003e\u003cp\u003e\n\tThen, I went straight to replacing my \u003ca href=\"/blog/2022-12-31#unicode-2022-12-31\"\u003e📝 makeshift locale-independent file I/O abstraction\u003c/a\u003e with \u003ca href=\"https://wiki.libsdl.org/SDL3/CategoryIOStream\"\u003eSDL's always-UTF-8 counterpart\u003c/a\u003e. And oh boy did this reveal how terrible my code actually was, particularly due to its aim of naturally supporting C++ data structures. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Switching to a more traditional stream-based API not only allowed me to delete all these abstractions, but surprisingly also simplified most file I/O call sites. Sure, a C API means no more well-defined lifetimes and forces me to manually close streams again, but Shuusou Gyoku doesn't do \u003ci\u003ethat\u003c/i\u003e much file I/O for that to be even just a slight annoyance. I sure didn't feel the need for a wrapper class, but I \u003ci\u003edid\u003c/i\u003e feel the need for \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/platform/sdl/file_sdl.cpp\"\u003e\u003ccode\u003echar8_t*\u003c/code\u003e wrappers\u003c/a\u003e that made SDL's file functions work more naturally with \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/game/narrow.h\"\u003ethe strong type-level distinction between UTF-8 and packfile-originating Shift-JIS\u003c/a\u003e I applied to the rest of the code.\u003cbr\u003e\n\tIn the end, \u003ca href=\"/blog/2025-05-20#filesizes-2025-05-20\"\u003e📝 timestamp preservation\u003c/a\u003e is the only remaining file-related feature that still requires custom platform-specific code, since it's most certainly outside of SDL's scope. It originally seemed as if I also needed to keep exclusive file opening for screenshots as SDL had no way of specifying \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#create_new\"\u003e\u003ccode\u003eCREATE_NEW\u003c/code\u003e on Windows\u003c/a\u003e, but since \u003ccode\u003efopen(…, \"wx\")\u003c/code\u003e did work in an undocumented way on all of SDL's other automatically tested platforms, \u003ca href=\"https://github.com/libsdl-org/SDL/pull/14165\"\u003eit made sense to just turn this into an officially supported feature and provide the missing Windows implementation\u003c/a\u003e.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tUnfortunately, SDL 3 turned out to be not as perfect as it seemed in April after all:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tMost of \u003ca href=\"https://wiki.libsdl.org/SDL3/CategoryStdinc\"\u003eSDL's C standard library function implementations\u003c/a\u003e do not deliver on the promise of a consistent implementation across platforms because they fall back on the compiler's C runtime by default. This may make sense for all the floating-point functions, which behave in largely unsurprising ways. In that case, you might as well use a compiler's own optimized implementation, sure. But doing the same for all the string functions, where we \u003ci\u003edo\u003c/i\u003e want consistent behavior that \u003ci\u003eisn't\u003c/i\u003e forced to implement the \u003ca href=\"https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f027338b0fab0f5078971fbe\"\u003elocale braindeath\u003c/a\u003e mandated by the C standard? Or maybe this \u003ci\u003eis\u003c/i\u003e preferable after all if you consider the subtle limitations in SDL's rather lazily implemented replacements, like how the \u003ca href=\"https://github.com/libsdl-org/SDL/blob/cbcb145eb4d0b1a9ef2f57a837d42fef1f930b65/src/stdlib/SDL_string.c#L2030\"\u003e\u003ccode\u003eprintf()\u003c/code\u003e family prints an undefined value if the integer portion of a \u003ccode\u003efloat\u003c/code\u003e value exceeds the range of an \u003ccode\u003eunsigned long long\u003c/code\u003e\u003c/a\u003e? This probably hurts more applications than the rare actual effects of locale braindeath ever could. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tSure, we \u003ci\u003ecould\u003c/i\u003e configure our Windows builds to opt into these replacements, but we don't have that control on Linux where we'd better use a distribution's SDL package. If I do start using them one day, it's purely because it removes the statically linked implementations from the game binary.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe Windows implementation of SDL's file I/O functions uses buffered I/O for reading, but \u003ca href=\"https://github.com/libsdl-org/SDL/issues/12424\"\u003enot for writing\u003c/a\u003e. This isn't much of a problem for Shuusou Gyoku itself because all of the file formats written by game logic just consist of a rather small number of buffers.\u003cbr\u003e\n\tBut it does become a problem in conjunction with \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_SaveBMP\"\u003e\u003ccode\u003eSDL_SaveBMP()\u003c/code\u003e\u003c/a\u003e, which we'd like to use for .BMP screenshots due to its support for any possible pixel format. My previous implementation for pbg's unpublished .BMP saving code evolved out of the debug code I quickly cobbled together while I was \u003ca href=\"/blog/2023-08-01#ddraw-2023-08-01\"\u003e📝 reverse-engineering all the porting-relevant surface management details of DirectDraw\u003c/a\u003e. This code was (and still is) limited to the pixel formats that .BMP most naturally supports, which was fine since those formats exactly matched the ones used by DirectDraw's framebuffer at all relevant bit depths, at least on my machine. Thanks to my \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/game/endian.h\"\u003eobjectively correct solution for handling endianness at the type level\u003c/a\u003e, this code even has well-defined byte order for the header fields, and thus works just as well on big-endian systems. After naturally filling in two structures, the code can then simply \u003ca href=\"https://github.com/nmlgc/ssg/blob/882bdc1dd640fcf24d8dac75e1f03b8fea810985/game/format_bmp.cpp#L131-L134\"\u003ewrite out the entire .BMP file within four write calls\u003c/a\u003e. Even with my previously equally unbuffered file I/O functions, it doesn't get much faster than that.\u003cbr\u003e\n\tSDL's .BMP writer, on the other hand, was implemented with the exact opposite set of priorities: With \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_ConvertPixels\"\u003ea pixel format converter as part of the library\u003c/a\u003e, it can always convert any image into a .BMP-compatible format. But then, it decided to shift byte order handling to the I/O subsystem, using \u003ca href=\"https://github.com/libsdl-org/SDL/blob/e2195621d792eb9a9f1268d31625ad35bbc3f927/src/video/SDL_bmp.c#L744-L786\"\u003eone write call for each individual field within the .BMP header\u003c/a\u003e. And if those write calls aren't buffered and get directly translated into Win32 \u003ccode\u003eWriteFile()\u003c/code\u003e syscalls, well…\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"bmp-2025-10-19 numbers\"\u003e\n\t\u003cthead style=\"vertical-align: bottom;\"\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003eSurface\u003cbr\u003econversion\u003c/th\u003e\n\t\t\t\u003cth\u003eHeader\u003c/th\u003e\n\t\t\t\u003cth\u003ePixels\u003c/th\u003e\n\t\t\t\u003cth\u003eTotal\u003c/th\u003e\n\t\t\t\u003cth\u003eNotes\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e8-bit, fast\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.008 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e16-bit, fast\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.095 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003eonly supports XRGB1555\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e32-bit, fast\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.164 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e8-bit, SDL\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e8.196 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e1.972 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e10.169 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e16-bit, SDL\u003c/th\u003e\n\t\t\t\u003ctd\u003e2.154 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.299 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e3.166 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e5.619 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003egets converted to 24-bit\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e32-bit, SDL\u003c/th\u003e\n\t\t\t\u003ctd\u003e0.753 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.279 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e3.162 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003e4.194 ms\u003c/td\u003e\n\t\t\t\u003ctd\u003egets converted to 24-bit\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tDurations of saving an already retrieved 640×480-pixel buffer on the same Windows system.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tIt's quite hilarious to see SDL getting slower as the bit depth \u003ci\u003edecreases\u003c/i\u003e. If SDL ends up calling \u003ccode\u003eWriteFile()\u003c/code\u003e \u003ca href=\"https://github.com/libsdl-org/SDL/blob/e2195621d792eb9a9f1268d31625ad35bbc3f927/src/video/SDL_bmp.c#L788-L803\"\u003e1024 times to save the palette for an 8-bit image one byte at a time\u003c/a\u003e, it's no wonder that writing the header takes 4× as long as writing the pixel data itself. With that much of a performance difference, removing my previous fast path would be an unacceptable downgrade. This is why the P0326 build only uses SDL's .BMP writer if it absolutely has to.\n\u003c/p\u003e\u003cp\u003e\n\tThat said, we could definitely improve SDL's .BMP writer to get the best of both worlds. Aside from adding general write buffering for Windows, I could add some of my fast paths, or even cover 16-bit RGB565 using .BMP's obscure \u003ca href=\"https://www.virtualdub.org/blog2/entry_177.html\"\u003e\u003ccode\u003eBI_BITFIELDS\u003c/code\u003e feature\u003c/a\u003e rather than upconverting such images to 24-bit RGB888. If you like ReC98 being used as a means to get me to make much more globally valuable contributions to SDL, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/93\"\u003ethis is the issue you want to fund\u003c/a\u003e. If you do primarily care about \u003cspan class=\"hovertext\" title=\"Remember, everything we implement for Shuusou Gyoku now will be reused in the future ports of PC-98 Touhou!\"\u003ethe games\u003c/span\u003e though, it might still be worth it – then, we could save 32-bit screenshots as 24-bit .BMPs, making them 25% smaller with not \u003ci\u003ethat\u003c/i\u003e much of a reduction in saving performance.\u003cbr\u003e\n\tThen again, having access to \u003ccode\u003eSDL_ConvertPixels()\u003c/code\u003e in logic code means that we now even support arbitrary pixel formats for WebP, and computers are only going to get faster… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tThese pushes only scratched the surface, and there's still a bit to do in terms of fully embracing SDL and removing redundant glue code. For example, we now load \u003ca href=\"/blog/2023-09-30#resampling-2023-09-30\"\u003e📝 Shuusou Gyoku's packed sound effect files\u003c/a\u003e using SDL's .WAV loader, which removed miniaudio's integrated \u003ca href=\"https://github.com/mackron/dr_libs/blob/24d738be2349fd4b6fe50eeaa81f5bd586267fd0/dr_wav.h\"\u003edr_wav\u003c/a\u003e along with the C runtime's \u003ccode\u003efopen()\u003c/code\u003e implementation it forcibly depends on, but we still use miniaudio for both sound mixing and output. Swapping out these libraries takes more testing effort than you might think, and I had to stop somewhere. For now, I got everything out of this that I wanted, and it's time to go back to working on actual features.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tIn a final bit of SDL-unrelated and more wholesome news, the Windows 98 port now makes sure to actually pick MS Gothic on non-Japanese systems instead of potentially falling back on the different and possibly Mincho-styled font you might have seen in \u003ca href=\"/blog/2025-04-25#d3dwindower-2025-04-25\"\u003e📝 the screenshots for my first Windows 98 release\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure style=\"width: 646px\"\u003e\u003crec98-child-switcher\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-10-19-SH01-Windows-98-MS-Mincho.webp?bef3167f\"\n\t\tdata-title=\"P0310-2\"\n\t\twidth=\"646\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu as rendered by the P0310-2 build on Windows 98, falsely using MS Mincho rather than MS Gothic for all its text\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-10-19-SH01-Windows-98-MS-Gothic.webp?0270eec8\"\n\t\tdata-title=\"P0326\"\n\t\twidth=\"646\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu as rendered by the P0326 build on Windows 98, now using the correct MS Gothic font\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd that's it for now!\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0326\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01n-032.png?38cbcd51\" alt=\":sh01n:\" width=\"24\" height=\"24\" \n\t\t\tsrcset=\"/static/emoji-sh01n-016.png?b4ffeada 0.66x, /static/emoji-sh01n-032.png?38cbcd51 1.33x, /static/emoji-sh01n-048.png?67a1addc 2x, /static/emoji-sh01n-128.png?96524b5d 5.33x\"\u003e Shuusou Gyoku P0326 Windows build\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://aur.archlinux.org/packages/seihou-shuusou-gyoku\"\u003eShuusou Gyoku P0326 on the AUR\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://flathub.org/apps/net.nmlgc.rec98.sh01\"\u003eShuusou Gyoku P0326 on Flathub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up: No more delays, no more excuses, it's finally time for the long-expected big look at TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e! I've long dreaded this moment because every time I've looked at that binary, I saw highly intertwined foundational gameplay features that made it hard to focus on just a single thing. But now that netplay hype has accumulated plenty of budget, I can take a more extended look at all of these aspects, or even cover all of them in one big delivery if need be.\u003cbr\u003e\n\tThe goal of netplay also guides my RE efforts into two more specific directions:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eIdentifying \u003ca href=\"/blog/2025-05-10#tuning-2025-05-10\"\u003e📝 the two remaining difficulty-controlling variables\u003c/a\u003e is crucial before we can even start working on netplay.\u003c/li\u003e\n\t\u003cli\u003eIdentifying data at the top and bottom edges of the currently un-RE'd portion of the data segment can help with optimizing rollback. If that data is constant, we can reduce the amount of per-frame data saved in the rollback buffer, increasing performance and rollback times.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tOr maybe it makes more sense to just go for the AI, enemy, or pattern code commissioned by LeyDud instead? We'll see.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-12-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-10-19T21:52:40Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-09-29",
      "url": "https://rec98.nmlgc.net/blog/2025-09-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-10-19\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-09-29\"\u003e\u003ctime datetime=\"2025-09-29T23:55:58Z\"\u003e2025-09-29 23:55\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0319\"\u003eP0319\u003c/a\u003e\n\t\t\tDebloating / portability (TH02/TH03/TH04/TH05 cutscenes / TH04/TH05 High Score viewer / TH04/TH05 player selection / TH04/TH05 setup menu)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b2c520b...8176042\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6734fa1...ffaca84\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0320\"\u003eP0320\u003c/a\u003e\n\t\t\tDebloating / portability (TH02/TH03/TH04/TH05 Music Room / TH02/TH03/TH04/TH05 title screen and main menu / TH02 shot type selection)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8176042...aaab441\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ffaca84...4037e21\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0321\"\u003eP0321\u003c/a\u003e\n\t\t\tTH02-TH05 debloating (Scorefile cleanup / Merging OP.EXE and MAINE.EXE/MAINL.EXE)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/aaab441...c3b6af2\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4037e21...0d9b018\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0322\"\u003eP0322\u003c/a\u003e\n\t\t\tTH02-TH05 debloating (Replicating GAME.BAT in C++ / Reducing memory requirements of merged binaries)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c3b6af2...6043093\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0d9b018...dd99c23\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0323\"\u003eP0323\u003c/a\u003e\n\t\t\tTH03 debloating (Main menu) / TH03 Anniversary Edition (Fork banner / VS menu quit choice)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6043093...3130b0a\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dd99c23...6729270\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/7a594f7...c82f955\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debloating\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Cleaning up ZUN\u0026#39;s original code into something you can actually read and maintain.\"\u003edebloating\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/anniversary-edition\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Polished and bugfixed forks of 100% decompiled games.\"\u003eanniversary-edition\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\n\n\n\n\u003cstyle\u003e\n\t.fragmentation-2025-09-29 td:not(:last-child),\n\t.fragmentation-2025-09-29 th:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\n\t.fragmentation-2025-09-29 th {\n\t\tvertical-align: bottom;\n\t}\n\n\t.heap-2025-09-29 tbody {\n\t\tfont-family: monospace;\n\t}\n\n\t.bundle-2025-09-29 {\n\t\tbackground-color: var(--c-bg);\n\t}\n\t.bundle-2025-09-29 td:not(:first-child) {\n\t\tfont-family: monospace;\n\t}\n\t.bundle-2025-09-29 tr th:nth-child(3),\n\t.bundle-2025-09-29 tr td:nth-child(3),\n\t.bundle-2025-09-29.naive-2025-09-29 tr th:nth-child(5),\n\t.bundle-2025-09-29.naive-2025-09-29 tr td:nth-child(5) {\n\t\tborder-right: var(--table-border);\n\t\tborder-right-width: 2px;\n\t}\n\t.bundle-2025-09-29 th:not(:last-child),\n\t.bundle-2025-09-29 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\n\t#kaja-2025-09-29 th:not(:last-child),\n\t#kaja-2025-09-29 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tPart 4! Let's get this over with, fix all these landmines, and conclude \u003ca href=\"/blog/2025-09-06\"\u003e📝 this 4-post series about the big 2025 PC-98 Touhou portability subproject\u003c/a\u003e. Unless I find something big in TH03, this is likely to be the final long blog post for this year. I wanna code again!\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#scope-2025-09-29\"\u003eThe scope\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#retain-2025-09-29\"\u003eRetaining menu backgrounds in conventional RAM\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#fragment-2025-09-29\"\u003eFighting heap fragmentation\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#tearing-2025-09-29\"\u003eResolving screen tearing landmines\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#vblank-2025-09-29\"\u003eDeterministically running tasks in VBLANK\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tearing-easy-2025-09-29\"\u003eAn easy example (TH04/TH05 main menu reinitialization)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tearing-hard-2025-09-29\"\u003eA hard example (TH03 character selection)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tearing-lunatic-2025-09-29\"\u003eA lunatic example (TH02 High Score screen)\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#unused-2025-09-29\"\u003eIntermission: Handling unused code on fork branches\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#merge-2025-09-29\"\u003eMerging TH02-TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#scorefiles-2025-09-29\"\u003eScorefile inconsistencies\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sizes-2025-09-29\"\u003eKeeping TH05's binary size low\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#game-bat-2025-09-29\"\u003eReplicating TH02-TH05's \u003ccode\u003eGAME.BAT\u003c/code\u003e in C++\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#prespawn-2025-09-29\"\u003eCalculating correct resident sizes\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#obstacles-2025-09-29\"\u003eAdditional features and obstacles\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#th03-vs-quit-2025-09-29\"\u003eTopping it off with an actual feature\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"scope-2025-09-29\"\u003e\u003cp\u003e\n\tAs you can already tell by this table of contents, this \"initial\" cleanup work was quite larger in scope than its counterpart for \u003ca href=\"/blog/2023-03-05\"\u003e📝 the first TH01 Anniversary Edition release\u003c/a\u003e. Even that already took unexpectedly long 2½ years ago, and now imagine doing that across four games simultaneously while keeping all the little required inconsistencies in place. Then you'll get why this has taken over four months…\u003cbr\u003e\n\tWith an overall goal of \"general portability\", it's very tempting to escalate the scope towards covering \u003ci\u003eeverything\u003c/i\u003e in these menu and cutscene binaries. So I had to draw at least \u003ci\u003esome\u003c/i\u003e boundaries:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eNo big feature work in TH01\u003c/li\u003e\n\t\u003cli\u003eNo work on any graphics formats besides PI\u003c/li\u003e\n\t\u003cli\u003eGraphical text will still get rendered directly to VRAM\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tEven then, this was way premature. Not only because we still need to maintain the memory layout of TH02's and TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e, but also because of all the undecompilable ASM code in all four games that blocks certain architectural simplifications.\u003cbr\u003e\n\tThe biggest problem, however: I haven't quite decided on how to use static libraries within my build environment yet. Since Turbo C++ 4.0J's linker just blindly includes every explicitly named object file without eliminating dead code, static libraries are essential for reducing bloat by providing a layer of optional files to be included on demand. However, the Windows and DOS versions of TLIB are easily confused, TLIB's usual paradigm of mutating existing library files goes against Tup's explicit dependency graph, and should we \u003ci\u003ereally\u003c/i\u003e depend on an ancient proprietary tool for a job that I could reimplement in a few hundred lines? Famous last words, I know. But since I \u003ca href=\"/blog/2025-01-25#ipapatch-2025-01-25\"\u003e📝 didn't want to do any dedicated build system work this year\u003c/a\u003e, I also didn't want to sort out these questions in a 12\u003csup\u003eth\u003c/sup\u003e or even 13\u003csup\u003eth\u003c/sup\u003e push. Leaving the build environment woefully ill-equipped for the complexity of this task was probably a mistake; while the resulting workaround of \u003ca hef=\"https://github.com/nmlgc/ReC98/blob/6729270d15026400ed98643e676be1596a56ea42/Tupfile.lua#L307-L355\"\u003efeature bundles\u003c/a\u003e does the job, it's very silly and hopefully won't last very long. I'm \u003ci\u003edefinitely\u003c/i\u003e going to spend some time sorting out the static library situation before I ever attempt something like this again. Or at some general point before we hit the overall 100% finalization mark, because we've still got that long-awaited librarization of ZUN's master.lib fork ahead of us.\n\u003c/p\u003e\u003chr id=\"retain-2025-09-29\"\u003e\u003cp\u003e\n\tLet's get to it then, starting with the feature that will remove lag in menus by removing PC-98-specific page-flipping and EGC code:\n\u003c/p\u003e\u003ch3\u003eRetaining menu backgrounds in conventional RAM\u003c/h3\u003e\u003cp\u003e\n\tAt first, this seems to be no problem. We just swap out master.lib's .PI functions with our forked PiLoad and our generic blitter, and make sure to keep the images allocated. master.lib's \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e has always loaded .PI images into one big contiguous buffer in conventional RAM, so this shouldn't negatively affect the heap layout. If anything, we'd be \u003ci\u003esaving\u003c/i\u003e memory by \u003ca href=\"/blog/2025-09-10#pi-attempts-2025-09-10\"\u003e📝 not allocating these extra two rows\u003c/a\u003e, right?\u003cbr\u003e\n\tUnfortunately, it's that second goal that would turn out to be a massive problem. \u003ca href=\"/blog/2025-09-06#pages-2025-09-06\"\u003e📝 The end of part 1\u003c/a\u003e already hinted at how the majority of menu backgrounds are only rendered to VRAM a single time before ZUN immediately frees them from memory. These cases are so common that I defined a macro for them:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e#define pi_fullres_load_palette_apply_put_free(slot, fn) { \\\n\tpi_load(slot, fn); \\\n\tpi_palette_apply(slot); \\\n\tpi_put_8(0, 0, slot); \\\n\tpi_free(slot); \\\n}\u003c/pre\u003e\n\t\u003cfigcaption\u003eAt the current state of decompilation, this macro is used 16 times across TH02-TH05, and it will appear an additional 12 times by the time decompilation is done.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIn these cases, the games only need that single 128\u0026nbsp;KiB block temporarily, and then get to reuse that memory for other, more dynamic graphics. Consequently, ZUN probably dimensioned the master.lib heap sizes for TH02-TH05 to leave ample headroom with this fact in mind. I wasn't so sure about deliberately limiting the amount of heap memory \u003ca href=\"/blog/2021-11-29\"\u003e📝 in late 2021 when I fixed the one out-of-memory landmine that remained in TH04\u003c/a\u003e, but I've begun to appreciate these memory limits quite a lot as the scope of my research has deepened. Specify the right amount of bytes, perform the single allocation from the DOS heap at startup, and if that allocation succeeds, you've removed an entire class of out-of-memory bugs from consideration. Sure, modders might prefer \u003ccode\u003emem_assign_all()\u003c/code\u003e for simplicity during development, but it does make sense to return to a static limit when shipping. For once, ZUN was right, and there is no excuse. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tOn the surface, this macro is equivalent to PiLoad's original direct-to-VRAM approach. And indeed, we \u003ci\u003ecan\u003c/i\u003e replace this code with a call to PiLoad's original code path in the few cases where we just want to show a static image without unblitting any of its regions later on, removing even the requirement for that temporary 128\u0026nbsp;KiB block in the process. But in the majority of cases, we \u003ci\u003edo\u003c/i\u003e need these images in RAM, and ZUN's original heap sizes simply weren't intended for that.\u003cbr\u003e\n\tBut how much of a problem are ZUN's limits in practice? Well, there's at least one instance where retaining all images would require significantly more memory than ZUN anticipated. TH02's \u003ccode\u003eOP.EXE\u003c/code\u003e requests a 256,000-byte master.lib heap, but then wants to do this:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH02-Title-animation-original-66-MHz.webp?680995d6\" preload=\"none\" controls data-title=\"Original game\" data-active width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"138\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH02-Title-animation-original-66-MHz.avi?d7a8afb4\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH02-Title-animation-original-66-MHz.webm?2430abde\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH02-Title-animation-original-66-MHz.webm?c8fc2e7e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH02-Title-animation-original-66-MHz.webm?59684432\" type=\"video/webm\"\u003eVideo of the flashing 東方封魔録 animation before TH02's title screen, as rendered by the original game on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. Showcases screen tearing and slow .PI blitting. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH02-Title-animation-original-66-MHz.avi?d7a8afb4\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"25\" data-title=\"End of flash animation\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"137\" data-title=\"In main menu\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH02-Title-animation-fixed-66-MHz.webp?680995d6\" preload=\"none\" controls data-title=\"Debloated P0323 build\" width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"138\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH02-Title-animation-fixed-66-MHz.avi?95fefd55\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH02-Title-animation-fixed-66-MHz.webm?e70c9846\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH02-Title-animation-fixed-66-MHz.webm?38f4a207\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH02-Title-animation-fixed-66-MHz.webm?f00e09ed\" type=\"video/webm\"\u003eVideo of the flashing 東方封魔録 animation before TH02's title screen, as rendered by the debloated P0323 build on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. Showcases how the performance improvements and screen tearing fixes now render the animation exactly as ZUN defined it to look. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH02-Title-animation-fixed-66-MHz.avi?95fefd55\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"19\" data-title=\"End of flash animation\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"128\" data-title=\"In main menu\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf you step through this video, you'll see that this effect indeed page-flips between the later menu background (128,000 bytes) and the three images with the resized text (3\u0026nbsp;× 54,720\u0026nbsp;= 164,160 bytes). Hence, we must have already loaded all four of them into the heap before this animation starts. While the menu's main text is rendered on the text layer, its shadow is part of the graphics layer and must be unblitted when switching to the option menu and back, thus requiring this image in at least \u003ci\u003esome\u003c/i\u003e portion of memory.\u003cbr\u003e\n\tSince VRAM page 1 always shows the unmodified menu background image, we could cheat, use PiLoad's direct-to-VRAM code path, and then do a second load of the image to conventional RAM on frame 19, before the white-in palette effect. \u003ca href=\"/blog/2025-09-06#fp-2025-09-06\"\u003e📝 Frame-perfection rule #2\u003c/a\u003e would also allow us to do that. But adding a second load time is lame, especially because the white-in effect wouldn't even hide \u003ci\u003ethat\u003c/i\u003e many expensive calls in the debloated build anymore. After replacing the original final \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e call for the main menu image with a faster planar blit, it only hides the draw calls for the \u003ccode\u003e©ZUN\u003c/code\u003e text and some minor file and port I/O to load and apply the menu's final 48-byte palette from \u003ccode\u003eOP.RGB\u003c/code\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tThere's really no reason against just increasing the size of the master.lib heap to incorporate all of these four images at the same time. But how much additional memory do we actually need? Obviously, these four images are not the only allocations on the heap, which also needs to fit at least the following buffers:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe 8,192-byte snapshot of the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e loaded before the game, which are restored upon quitting the game… as well as when switching between game binaries. Yup – since the master.lib heap is not retained across binary switches, every one of the three binaries restores the system's previous gaiji before being switched out, before the new binary reads those same gaiji from the character generator back onto the master.lib heap. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e It would have been much smarter to keep them in a separate persistent allocation on the DOS heap instead.\u003c/li\u003e\n\t\u003cli\u003eThe 16,384-byte .PI load buffer. This one has to be allocated \u003ci\u003ebefore\u003c/i\u003e we allocate any .PI, so it will necessarily fragment away that memory.\u003c/li\u003e\n\t\u003cli\u003eThe 2,560 bytes of High Score menu sprites loaded from \u003ccode\u003eop_h.bft\u003c/code\u003e. We might have shown the High Score menu if we came from a demo, and ZUN wants to keep these sprites loaded across the lifetime of the process.\u003c/li\u003e\n\t\u003cli\u003eThe 9,216-byte \u003ccode\u003esuper_buffer\u003c/code\u003e that master.lib pre-allocates upon loading the first BFNT sprite, just in case you later want to call \u003ccode\u003esuper_convert_tiny()\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWe can bypass the \u003ccode\u003esuper_buffer\u003c/code\u003e allocation \u003ca href=\"https://github.com/nmlgc/ReC98/blob/6729270d15026400ed98643e676be1596a56ea42/th02/op_04.cpp#L146-L160\"\u003ethrough a dumb trick\u003c/a\u003e. But the single worst aspect hides \u003ci\u003ebetween\u003c/i\u003e all these individual allocations:\n\u003c/p\u003e\u003ch4 id=\"fragment-2025-09-29\"\u003eFighting heap fragmentation\u003c/h4\u003e\u003cp\u003e\n\tLet's look at TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e, whose heap limit of 336,000 bytes is much more lenient. This limit should be more than enough to fit the new additional 128,000-byte buffer for the background image in addition to the original heap contents on every menu screen, and we can indeed enter the main menu without any issue. But then, we're still greeted with an out-of-memory crash after entering and leaving the Music Room? Let's take a look into the master.lib heap, with the retained background images we'd like to have:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ctable class=\"fragmentation-2025-09-29\"\u003e\n\t\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eStep\u003c/th\u003e\n\t\t\t\t\u003cth colspan=\"4\"\u003eHeap layout\u003c/th\u003e\n\t\t\t\t\u003cth style=\"width: 7ch;\"\u003eTotal remaining\u003c/th\u003e\n\t\t\t\t\u003cth style=\"width: 7ch;\"\u003eLargest free block\u003c/th\u003e\n\t\t\t\t\u003cth style=\"width: 7ch;\"\u003eFragmentation loss\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/thead\u003e\n\t\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eEntering the main menu\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003eop1.pi\u003c/code\u003e (128,000)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003esft*.cd2\u003c/code\u003e + \u003ccode\u003ecar.cd2\u003c/code\u003e (7,360)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003esl*.cdg\u003c/code\u003e (90,240)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003escnum.bft\u003c/code\u003e + \u003ccode\u003ehi_m.bft\u003c/code\u003e (7,680)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e76,352\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e66,240\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e10,112\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003eLeaving the main menu\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003escnum.bft\u003c/code\u003e + \u003ccode\u003ehi_m.bft\u003c/code\u003e (7,680)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e301,984\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e234,608\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e67,376\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003eEntering the Music Room\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003emusic.pi\u003c/code\u003e (128,000)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u003ca href=\"/blog/2024-02-03#colors-2024-02-03\"\u003e📝 nopoly_B\u003c/a\u003e\u003c/code\u003e (32,000)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003escnum.bft\u003c/code\u003e + \u003ccode\u003ehi_m.bft\u003c/code\u003e (7,680)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e139,536\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e66,240\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e73,296\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003eLeaving the Music Room\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003emusic.pi\u003c/code\u003e (!) (128,000)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003escnum.bft\u003c/code\u003e + \u003ccode\u003ehi_m.bft\u003c/code\u003e (7,680)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e165,552\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e81,920\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e83,632\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003eRe-entering the main menu\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e.PI load buffer (16,384)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003esft*.cd2\u003c/code\u003e + \u003ccode\u003ecar.cd2\u003c/code\u003e (7,360)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003esl*.cdg\u003c/code\u003e (90,240)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003ccode\u003escnum.bft\u003c/code\u003e + \u003ccode\u003ehi_m.bft\u003c/code\u003e (7,680)\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e179,760\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e\u003cstrong style=\"color: red\"\u003e115,648\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e64,112\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t\u003ci\u003eSomething\u003c/i\u003e gradually shreds our heap into tiny pieces that ultimately prevent us from allocating the main menu background image a second time. We can surely blame this on ZUN's suboptimal order of load calls that doesn't prioritize larger images over smaller ones, or on the 16\u0026nbsp;KiB .PI load buffer that we maybe should have allocated statically. But the biggest hidden offender turns out to be… master.lib's packfile implementation?!\n\u003c/p\u003e\u003cp\u003e\n\tYup. Every time you load a file out of an archive, master.lib heap-allocates a 31-byte state structure and a file read buffer that master.lib originally dimensioned at 520 bytes. TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e and \u003ccode\u003eMAINL.EXE\u003c/code\u003e then increased the size of that buffer to 4,104 bytes, before TH04 and TH05 went up to 8,200 bytes. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tUltimately though, it's not the size that's the problem here, but the fact that we repeatedly allocate \u003ci\u003eany\u003c/i\u003e memory that could have been allocated once when setting up the \u003ccode\u003eINT 21h\u003c/code\u003e handlers. You'd think that master.lib went for dynamic allocations in order to support the fact that the \u003ccode\u003eINT 21h\u003c/code\u003e file API lets you open multiple file handles simultaneously, which would point to different RLE-compressed files within the archive. But no, master.lib doesn't even support this case, and even incorrectly returns \u003ccode\u003eFileNotFound\u003c/code\u003e if you attempt to open a second file from an archive before closing the first one! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tAfter I identified this whole issue, I immediately wanted to replace master.lib's packfile code with the much saner and more explicit C++ implementation from TH01. Sooner or later, we'll have to do this anyway because we can't just hook file syscalls on other operating systems the way we can hook them on DOS.\u003cbr\u003e\n\tHowever, TH01's implementation would quickly turn out to have its own share of heap fragmentation issues. Every time the game loads an RLE-compressed file from \u003ccode\u003e東方靈異.伝\u003c/code\u003e, the archived file is completely decompressed into a newly allocated temporary buffer, from where the game then copies out parts into the actual game structures. The resulting fragmentation is at least easily fixable though, and that's what the TH01 part of the very first push assigned to part 1 went to. Switching to a zero-copy architecture basically only required persisting the RLE state and brought a significant improvement: 15,776 bytes of heap memory during Stage 1 freed up by that switch alone, as reported by the \u003ccode\u003ecoreleft()\u003c/code\u003e output seen in debug mode?! \u003ci\u003eThat\u003c/i\u003e much for just removing temporary allocations that the game was freeing anyway?\u003cbr\u003e\n\tLet's check the Borland C++ \u003ccite\u003eDOS Reference\u003c/cite\u003e for how this value is actually calculated. Turns out that it is simply intended to be \u003cq\u003ea measure of unused RAM memory\u003c/q\u003e, and sure enough:\n\u003c/p\u003e\u003cblockquote\u003eIn the large data models, \u003ci\u003ecoreleft\u003c/i\u003e returns the amount of memory between the highest allocated block and the end of memory.\u003c/blockquote\u003e\u003cp\u003e\n\tThat's a reasonably meaningful measurement that can be determined in constant time, compared to the 𝑂(𝑛) operation of finding the true total size of available heap memory by walking over every node.\n\u003c/p\u003e\u003cp\u003e\n\tIn the end though, rolling out this C++ implementation to the other four games was way premature and would have pushed this delivery way above 12 pushes. After all, both ZUN's \u003ci\u003eand\u003c/i\u003e master.lib's code is still full of \u003ccode\u003eINT 21h\u003c/code\u003e file syscalls that would all need to be replaced. Conditionally, even, given the two binaries that are not yet position-independent…\u003cbr\u003e\n\tFortunately, .PI loading itself is just as much of an issue \u003ci\u003eand\u003c/i\u003e can be worked around in the much simpler way I already spoiled when explaining \u003ca href=\"/blog/2025-09-10#pi-piload-2025-09-10\"\u003e📝 the API changes I made to PiLoad\u003c/a\u003e: We simply hold on to the menu background pixel buffer for as long as possible. Ideally, we only allocate these 128\u0026nbsp;KiB once, decode every new menu background into that same buffer, and only explicitly free it when we really need to. That's why the platform layer logic requires full control over .PI buffer allocation.\u003cbr\u003e\n\tThis was enough to keep the required amount of additional heap memory to a more than acceptable level:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tNone of the \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e binaries needed a larger memory limit.\n\u003c/li\u003e\u003cli\u003e\n\tTH04's \u003ccode\u003eOP.EXE\u003c/code\u003e also got to keep its original 336,000-byte heap.\n\u003c/li\u003e\u003cli\u003e\n\tAs did TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e. That one seems particularly surprising if you remember \u003ca href=\"/blog/2024-11-22#select-2024-11-22\"\u003e📝 its 255,216-byte character selection screen\u003c/a\u003e. But since ZUN only preloads 186,096 bytes before or during the main menu, we can nicely fit the title screen image into the original 352,000-byte heap.\n\u003c/li\u003e\u003cli\u003e\n\tTH05's \u003ccode\u003eOP.EXE\u003c/code\u003e needed a slight increase by 4,768 bytes to a new limit of 340,768 bytes, for the mere sake of accommodating the heap fragmentation caused by entering and leaving the Music Room and then entering its character selection screen. An increase at that level would have been fine even if it wasn't temporary, as we're still 52,832 bytes short of reaching the amount of memory required by \u003ccode\u003eMAIN.EXE\u003c/code\u003e:\n\t\u003cfigure\u003e\u003ctable class=\"heap-2025-09-29 numbers\"\u003e\n\t\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e (original)\u003c/th\u003e\n\t\t\t\t\u003cth\u003eStatic\u003c/th\u003e\u003cth\u003eHeap\u003c/th\u003e\u003cth\u003eTotal\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/thead\u003e\n\t\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eOP.EXE\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e 88,064\u003c/td\u003e\u003ctd\u003e336,000\u003c/td\u003e\u003ctd\u003e424,064\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eMAIN.EXE\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e190,464\u003c/td\u003e\u003ctd\u003e291,200\u003c/td\u003e\u003ctd\u003e481,664\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\u003c/figure\u003e\n\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThat only left TH02's \u003ccode\u003eOP.EXE\u003c/code\u003e, which did require a whopping additional 80,416 bytes up to a very similar new limit of 336,416 bytes. But again, an increase at that level is also fine for this game when compared against its \u003ccode\u003eMAIN.EXE\u003c/code\u003e, which would allow \u003ccode\u003eOP.EXE\u003c/code\u003e to go up to 383,232 bytes of heap without increasing the original memory requirements:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"heap-2025-09-29 numbers\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th02.png?c6bfc44e\" alt=\":th02:\" width=\"24\" height=\"24\" \u003e (original)\u003c/th\u003e\n\t\t\t\u003cth\u003eStatic\u003c/th\u003e\u003cth\u003eHeap\u003c/th\u003e\u003cth\u003eTotal\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eOP.EXE\u003c/th\u003e\n\t\t\t\u003ctd\u003e 69,632\u003c/td\u003e\u003ctd\u003e256,000\u003c/td\u003e\u003ctd\u003e325,632\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eMAIN.EXE\u003c/th\u003e\n\t\t\t\u003ctd\u003e164,864\u003c/td\u003e\u003ctd\u003e288,000\u003c/td\u003e\u003ctd\u003e452,864\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tNot to mention that our final merged \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e will only require 63,488 bytes of static memory, and that's \u003ci\u003edespite\u003c/i\u003e TH02 being the one game that received the quickest and sloppiest merge job with a lot of corners cut for budget reasons.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp id=\"tearing-2025-09-29\"\u003e\n\tAnd after a few rewritten function calls, we've indeed removed every single EGC-powered inter-page copy from all menus and cutscenes of TH02-TH05! On to the next goal…\n\u003c/p\u003e\u003chr\u003e\u003ch3 id=\"vblank-2025-09-29\"\u003eResolving screen tearing landmines\u003c/h3\u003e\u003cp\u003e\n\t…which requires individual solutions for every case that merely follow a common pattern. If things are already done close to a VSync wait loop and just in a slightly wrong order, the solution is easy and we just have to shift around a few function calls.\u003cbr\u003e\n\tBut what can we do if ZUN mutates visible pixels at some place far away from the last VSync wait loop? After all, a lot of these landmines result from confusing the current CRT beam position across multiple functions. Often, it's impossible to see at a glance where these menu-specific subfunctions are called within a frame without tracing execution back to the last VSync delay loop at the call site. For starters, it would be nice to clearly formalize that a specific section of code must be run in VBLANK.\n\u003c/p\u003e\u003cp\u003e\n\tmaster.lib's \u003ccode\u003evsync_Proc\u003c/code\u003e function pointer already gets us most of the way there. Its VSync subsystem automatically calls any non-\u003ccode\u003enullptr\u003c/code\u003e shortly after the VSync interrupt fires, and our task function would then set \u003ccode\u003evsync_Proc\u003c/code\u003e back to a \u003ccode\u003enullptr\u003c/code\u003e to ensure the intended one-shot behavior.\u003cbr\u003e\n\tHowever, this approach can at best defer a task to the \u003ci\u003enext\u003c/i\u003e VBLANK interval, which might leave us one frame behind the original game and hurt our frame-perfection goals. What we actually want is a conditional approach for timing-sensitive tasks, as a common operation that only requires a single line of code:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIf we're within VBLANK, run the task right now.\u003c/li\u003e\n\t\u003cli\u003eIf we aren't, set up a VSync proc that runs the task immediately after the next VSync and then removes the proc.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNow we're only missing that crucial one bit of information, which is delivered by Bit 5 of the graphics GDC's status register at I/O port \u003ccode\u003e0xA0\u003c/code\u003e. In fact, ZUN uses the same bit in all hand-written VSync wait code throughout TH01 and in the bouncing-ball ZUN Soft logo:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003evoid vsync_wait_via_gdc_polling(void)\n{\n\t// Bit 5 of the graphics GDC register indicates VBLANK. Wait until this bit\n\t// is set.\n\twhile((inportb(0xA0) \u0026 0x20) != 0) {\n\t}\n\n\t// Once Bit 5 is no longer set, the CRT has started drawing the next frame.\n\t// I have no idea why you would ever want to throw away all your precious\n\t// vertical retrace time, but ZUN does this all throughout TH01.\n\twhile((inportb(0xA0) \u0026 0x20) == 0) {\n\t}\n}\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tOf course, this only solves the problem in theory, as the tasks themselves don't come with any real-time guarantees. It's entirely possible for the resulting \u003ccode\u003evblank_run()\u003c/code\u003e function to get called near the end of VBLANK, start the task immediately, and return long after the CRT beam has started drawing again. Heck, if the system is slow enough, the task might not even complete within the VBLANK interval if we run it immediately after VSync. But this is a much more complex problem to solve, requiring upfront measurements of both the VBLANK interval \u003ci\u003eand\u003c/i\u003e the execution time for each potential task, which can then be factored into the run-now-or-defer decision. We definitely don't need to go there as long as we're mainly targeting emulated 66\u0026nbsp;MHz systems.\n\u003c/p\u003e\u003cp id=\"tearing-easy-2025-09-29\"\u003e\n\tIn easy cases, \u003ccode\u003evblank_run()\u003c/code\u003e can then resolve screen tearing landmines completely by itself. Towards the end of PC-98 Touhou, ZUN's menus made more and more use of master.lib's blocking palette fading functions, which delay themselves to the next VSync signal and thus avoid any tearing issues. Hence, TH04's and TH05's screen tearing landmines are limited to the very few sudden palette changes that remained in these games:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003evoid return_from_other_screen_to_main_menu(void)\n{\n\t// Loads the .PI image into our persistent menu image background buffer,\n\t// and overwrites master.lib's 8-bit palette. Takes a few frames and\n\t// probably won't return during VBLANK.\n\tGrpSurface_LoadPI(bgimage, \u0026Palettes, \"op1.pi\");\n\n\tgraph_accesspage(0);\n\tbgimage.write(0, 0); // Planar blit\n\tPaletteTone = 100;   // Use original brightness in palette_show()\n\u003cspan class=\"gd\"\u003e\n-\t// ZUN landmine: Updating the hardware palette right now will most likely\n-\t// cause screen tearing.\n-\tpalette_show();\u003c/span\u003e\u003cspan class=\"gi\"\u003e\n+\tvblank_run(palette_show);\u003c/span\u003e\n\n\t[…]\n}\u003c/pre\u003e\u003c/figure\u003e\u003cp id=\"tearing-hard-2025-09-29\"\u003e\n\tThe fixes for the landmines in TH03 and TH02, however, require much more thought and care to stay as close to ZUN's defined logical frame sequence as possible. TH03's character selection screen, which prompted this whole subproject in the first place, houses one of the harder groups of landmines:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH03-Select-landmines-original.webp?85fbb0e8\" preload=\"none\" controls data-title=\"Original game\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"213\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH03-Select-landmines-original.avi?0b8820d3\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH03-Select-landmines-original.webm?558d870f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH03-Select-landmines-original.webm?8a47cce7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH03-Select-landmines-original.webm?69f6684c\" type=\"video/webm\"\u003eVideo of TH03's character selection screen in 1P vs. CPU mode, selecting Reimu for both players, as rendered by the original game with all screen tearing landmines in place. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH03-Select-landmines-original.avi?0b8820d3\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"1\" data-title=\"💣 #1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"31\" data-title=\"💣 #2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"73\" data-title=\"💣 #3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"115\" data-title=\"💣 #3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"160\" data-title=\"💣 #4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH03-Select-landmines-fixed.webp?85fbb0e8\" preload=\"none\" controls data-title=\"Debloated P0323 build\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"213\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH03-Select-landmines-fixed.avi?05614cce\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH03-Select-landmines-fixed.webm?56da6007\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH03-Select-landmines-fixed.webm?05308a77\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH03-Select-landmines-fixed.webm?289e9980\" type=\"video/webm\"\u003eVideo of TH03's character selection screen in 1P vs. CPU mode, selecting Reimu for both players, as rendered by the debloated P0323 build with all screen tearing landmines fixed. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH03-Select-landmines-fixed.avi?05614cce\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"1\" data-title=\"💣 #1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"31\" data-title=\"💣 #2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"73\" data-title=\"💣 #3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"115\" data-title=\"💣 #3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"160\" data-title=\"💣 #4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tRecorded on DOSBox-X with 375,000 cycles, since exact machine specifications are not important to demonstrate these landmines.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Landmine #1 is caused by loading the \u003cspan lang=\"ja\"\u003eSelection\u003c/span\u003e BGM and the character name sprites before clearing VRAM, without a frame delay inbetween. The fix is obvious.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Landmine #2 already got mentioned in passing in the corresponding blog post from last year: \u003ca href=\"/blog/2024-11-22#tearing-2024-11-22\"\u003e📝 If a frame took longer than 3 VSync interrupts to render, ZUN flips the VRAM pages immediately without waiting for the next VSync interrupt\u003c/a\u003e. This always applies to the very first frame \u003ca href=\"/blog/2024-11-22#issues-2024-11-22\"\u003e📝 because the game thinks it took ≥30 VSync interrupts to render\u003c/a\u003e.\u003cbr\u003e\n\tIn both versions, we enter the loop as \u003ccode\u003evsync_Count1\u003c/code\u003e turns 30. But while the original game page-flips as soon as it finishes rendering in the middle of the screen frame, the debloated build instead waits for VSync during that 30\u003csup\u003eth\u003c/sup\u003e frame and only page-flips and resets the counter \u003ci\u003eafter\u003c/i\u003e VSync. This way, we still guarantee ZUN's originally defined 30 black frames preceding this menu, despite \u003ccode\u003evsync_Count1\u003c/code\u003e being one frame ahead in the debloated version of that delay.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Landmine #3 looks conceptually identical to landmine #2, being another mid-screen-frame page flip caused by \u003ccode\u003evsync_Count1\u003c/code\u003e being ≥3 by the time execution reaches the frame-rate-dropping delay loop. However, this one is caused by the game's response to a selection-confirming input and therefore needs a dedicated fix. Here's what's going on:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThe game renders the next frame to the invisible VRAM page, as it usually does. We won't see this frame for a while.\n\u003c/li\u003e\u003cli\u003e\n\tThe game checks for input and sees the Shot key. It then immediately runs the palette flash effect, using master.lib's blocking \u003ccode\u003epalette_white_in()\u003c/code\u003e while still displaying the previously rendered frame.\n\u003c/li\u003e\u003cli\u003e\n\t\u003ccode\u003epalette_white_in()\u003c/code\u003e itself avoids screen tearing issues by running its own VSync busy-waiting loops using the \u003ccode\u003e(inportb(0xA0) \u0026 0x20)\u003c/code\u003e check, without mutating \u003ccode\u003evsync_Count1\u003c/code\u003e. This is why the game immediately page-flips once execution is back to the main loop.\n\u003c/li\u003e\u003cli\u003e\n\t\u003ci\u003eHowever\u003c/i\u003e, this is \u003ci\u003enot\u003c/i\u003e the cause of this landmine. \u003ccode\u003epalette_white_in()\u003c/code\u003e also stops within VBLANK, so you'd also expect the immediate page flip to not cause any screen tearing.\n\u003c/li\u003e\u003cli\u003e\n\t\u003ci\u003eExcept\u003c/i\u003e that ZUN then (re-)loads the .CDG portrait for the selected or automatically assigned palette variant of the confirmed character, immediately after \u003ccode\u003epalette_white_in()\u003c/code\u003e, and only \u003ci\u003ethen\u003c/i\u003e drops back into the main loop, without any further delay. Hence, we have file I/O in our logical frame, and thus can't guarantee anything.\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tOn the hypothetical infinitely fast PC-98, the .CDG load call completes instantly and turns this into a non-issue. On real systems, however, we would need some way of hiding this load call to stick to ZUN's defined logical frames:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eLeaving it after \u003ccode\u003epalette_white_in()\u003c/code\u003e is completely wrong because it messes with the defined sequence of logical frames. Even just maintaining the original \u003ci\u003enumber\u003c/i\u003e of frames requires inserting an additional delay frame \u003ci\u003eand\u003c/i\u003e compensating for that by cutting that one frame from the next iteration of the loop. \u003ca href=\"https://x.com/ReC98Project/status/1926428420998639960\"\u003eThis was my original solution\u003c/a\u003e, and the realization of how wrong it was certainly delayed this blog post by about a day…\u003c/li\u003e\n\t\u003cli\u003eMoving it in front of \u003ccode\u003epalette_white_in()\u003c/code\u003e might work since the effect starts with a VSync wait, but it might also insert an additional screen frame of delay. Keep in mind that we're still on the same logical frame that rendered the very expensive curve effect.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThat only leaves one answer: Running both the white-in effect and the .CDG load concurrently. 💡 Using \u003ccode\u003evsync_Proc\u003c/code\u003e, we can implement a non-blocking version of \u003ccode\u003epalette_white_in()\u003c/code\u003e that runs one iteration of its palette-manipulating loop during VBLANK. Meanwhile, the \"main thread\" gets 16 frames to load a single 22.5\u0026nbsp;KiB character portrait and then simply waits for the white-in effect to complete. And since our VSync proc also always signals completion during VBLANK, we then get to immediately page-flip and retain ZUN's intended 3-frame timing. With this solution, we don't even have to optimize away the .CDG load in the usual case where the game just reloads the character's regular palette variant.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Landmine #4 is the palette tearing issue that got an entire section in the post from last year. After moving the palette-mutating branch from before VSync to immediately after VSync, we also have to adjust the calculation of the palette brightness value to match ZUN's original values.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tVery finicky work, where every single branch has the potential to introduce an off-by-one-frame error, and \u003ccode\u003evblank_run()\u003c/code\u003e doesn't help at all.\n\u003c/p\u003e\u003cp id=\"tearing-lunatic-2025-09-29\"\u003e\n\tAnd \u003ci\u003ethen\u003c/i\u003e you reach TH02, which asks for way too much to happen within a single frame, in plain sight, and with no palette tricks to hide it. The screen transitions into and out of its HiScore screen are by far the worst example:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH02-High-Score-landmines-original-33-MHz.webp?024ca523\" preload=\"none\" controls data-title=\"Original game\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"117\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH02-High-Score-landmines-original-33-MHz.avi?dfdf650b\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH02-High-Score-landmines-original-33-MHz.webm?22f06295\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH02-High-Score-landmines-original-33-MHz.webm?8ba24dd4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH02-High-Score-landmines-original-33-MHz.webm?34308a3b\" type=\"video/webm\"\u003eVideo of entering and leaving TH02's HiScore menu, as rendered by the original game on Neko Project 21/W with a clock speed of 1.9968 × 17 = 33.9456 MHz, showcasing a grand total of 6 screen tearing landmines and 2 bugs. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH02-High-Score-landmines-original-33-MHz.avi?dfdf650b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"First regular frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"57\" data-title=\"Key pressed\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"77\" data-title=\"\u003ccode\u003eOP2.PI\u003c/code\u003e load\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"98\" data-title=\"\u003ccode\u003eOP2.PI\u003c/code\u003e blit\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"111\" data-title=\"Page copy\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.webp?024ca523\" preload=\"none\" controls data-title=\"Debloated P0323 build\" loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"117\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.avi?9ceb8f5a\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.webm?301e19c4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.webm?c5900d31\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.webm?c67bd65d\" type=\"video/webm\"\u003eVideo of entering and leaving TH02's HiScore menu, as rendered by the debloated P0323 build on Neko Project 21/W with a clock speed of 1.9968 × 17 = 33.9456 MHz. Showcases how the screen tearing fixes even apply at this lower clock speed, and how retaining the title screen background image in memory allows an instant switch back to that menu. \u003ca href=\"/blog/static/video/zmbv/2025-09-29-TH02-High-Score-landmines-fixed-33-MHz.avi?9ceb8f5a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"First regular frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"57\" data-title=\"Key pressed\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"77\" data-title=\"Delay done\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"79\" data-title=\"Back to the main menu\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tRecorded at 1.9968\u0026nbsp;× 17\u0026nbsp;= 33.9456 MHz for a change to magnify the jank that you perhaps wouldn't see at higher clock speeds.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThese screen transitions exhibit no less than 6 landmines and 2 bugs:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Frame 1 shows how TRAM (containing the actual menu text as gaiji) gets cleared immediately, but VRAM (containing the shadow) remains untouched as ZUN decides to load \u003ccode\u003eHUUHI.DAT\u003c/code\u003e first.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 While the following VRAM clear \u003ci\u003eappears\u003c/i\u003e to produce a well-defined black frame 2, it's anything \u003ci\u003ebut\u003c/i\u003e well-defined, as the load operation only happens to conclude within VBLANK by sheer chance in this recording.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 Frame 4 is wild. First of all, the code still hasn't waited for a single VBLANK signal ever since entering the menu, and therefore shouldn't be writing to TRAM to begin with.\u003cbr\u003e\n\tBut even then, you wouldn't expect to see only the name and nothing else on the scanlines of a score record in such a partial rendering. How can TRAM operations possibly be \u003ci\u003ethat\u003c/i\u003e slow? This almost seemed as if I was missing some crucial timing-related detail about the hardware. But in the end, what we're seeing here is simply Neko Project not actually using scanline rendering for the text layer. If you write to a TRAM cell, Neko Project just marks the entire 16-pixel row to be redrawn during the next screen refresh event.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t🐞 Frame 5, then, is the first well-defined frame that actually renders the way it's defined in the code. The green \u003cspan lang=\"ja\" style=\"color: green\"\u003e東方封魔録\u003c/span\u003e logo is indeed only meant to be visible from the next frame onwards. This certainly meets all criteria for a bug, but the debloated build isn't allowed to fix those. In fact, it needs \u003ca href=\"https://github.com/nmlgc/ReC98/blob/6729270d15026400ed98643e676be1596a56ea42/th02/op_04.cpp#L117-L121\"\u003ea dedicated conditional branch\u003c/a\u003e to preserve this bug.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tOnce you leave the menu, you'll first have to sit through a stylistic and non-productive 20-frame delay, before… the screen switches back to the last frame rendered before the delay on frame 77?\u003cbr\u003e\n\t💣 By that point, we're technically already back to the main menu, where the first thing ZUN does is to switch from double-buffering back to single-buffering with VRAM page 0 shown. If you happened to leave the menu by hitting a key on the 50% of frames where VRAM page 1 is shown, the screen will therefore flip back to the frame rendered before the 20-frame delay, and keep it visible while master.lib decodes the title screen image.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t💣 This decoding process finishes after ~20.4 frames in this recording, near the middle of frame 98. Clearly, we then have to immediately switch the hardware palette to the one we just loaded. Let's completely disregard that we're probably not in VBLANK, or that the screen is still showing the last High Score menu frame… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThen, we need to get the image onto both VRAM pages. \u003ca href=\"/blog/2025-09-10#pi-piload-2025-09-10\"\u003e📝 As we found out in Part 2\u003c/a\u003e, a low-clocked 386 is pretty much the most suboptimal system for master.lib's packed→planar conversion code, and 12 frames exactly match the performance we would expect from Neko Project at 33\u0026nbsp;MHz.\u003cbr\u003e\n\t💣 But that only rendered the image to the invisible VRAM page 1. We could now temporarily show page 1 after the next VSync signal to hide the pretty much guaranteed multi-frame VRAM writes… but nah, who cares except for some researcher 28 years later. By leaving VRAM page 0 on screen, ZUN doesn't even attempt to hide the jank that is about to occur. Once again, he reaches for master.lib's \u003ccode\u003egraph_copy_page()\u003c/code\u003e, \u003ca href=\"/blog/2025-09-10#goal-2025-09-10\"\u003e📝 whose slowness I already talked about in Part 2\u003c/a\u003e. At 33\u0026nbsp;MHz, Neko Project takes 3 frames to copy one page after another, leaving us with two frames of mixed pixels. This can be even worse on real hardware: On \u003ca href=\"/blog/2025-09-10#xfade-res-2025-09-10\"\u003e📝 spaztron64's K6-2-upgraded and southbridge-bottlenecked PC-9821V166 model\u003c/a\u003e, this copy took 100\u0026nbsp;ms. I was able to watch every single bitplane getting individually copied in the recording. Unpacking the .PI image a second time would have been faster on that machine. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t🐞 Also, ZUN should have definitely cleared TRAM before the page copy instead of deferring this responsibility to the main menu rendering code. Since we then return to the main menu's VSync-timed loop and regularly wait for VSync while the scoreboard remains on screen and part of the current logical frame, this is not a landmine.\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tCompare this with the debloated version:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\tThe first three landmines are fixed by running the common \"set palette to black and clear TRAM\" operation in VBLANK, and deferring both the palette update and the scoreboard rendering to the VBLANK interval preceding frame 5.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tEverything between frame 77 and frame 113 inclusive is defined to happen on a single logical frame. Since this screen doesn't allocate its own 640×400 background, we get to keep the title screen image in memory and actually turn this logical frame into a real one. Then, we can use ZUN's defined 20-frame delay constructively:\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eFirst, we render the last frame to the other VRAM page to defuse landmine #5. Yes, \u003ci\u003erender\u003c/i\u003e – a GRCG-accelerated 385×209-pixel flood fill followed by eight transparent 16-color 128×32 sprites is much faster than copying VRAM pages.\u003c/li\u003e\n\t\t\u003cli\u003eThen, we can unconditionally switch to showing VRAM page 1 and accessing VRAM page 0 on the next VSync, without affecting what's shown on screen.\u003c/li\u003e\n\t\t\u003cli\u003eThen, we have all the time in the world to blit the planar title screen image from memory to VRAM page 0, the only one we still need to touch.\u003c/li\u003e\n\t\u003c/ul\u003e\u003cp\u003e\n\tOn the VSync that precedes frame 77, we then simply 🫰 flip VRAM pages and the hardware palette to produce exactly the well-defined image that an infinitely fast PC-98 would have produced for ZUN's original code.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tThen, I did that 13 more times for the other screen tearing landmines fixed in this build. And no, these new builds don't even fix every instance of this issue…\n\u003c/p\u003e\u003chr\u003e\u003ch3 id=\"unused-2025-09-29\"\u003eIntermission: Handling unused code on fork branches\u003c/h3\u003e\u003cp\u003e\n\tGiven that all of these improvements are taking place on the \u003ccode\u003edebloated\u003c/code\u003e branch, it's time to decide on how to handle the biggest unneeded obstacle in the way of our portability efforts, after \u003ca href=\"/blog/2023-03-05#single-2023-03-05\"\u003e📝 I procrastinated this question 2½ years ago\u003c/a\u003e.\u003cbr\u003e\n\tIn Shuusou Gyoku, I've been trying to retain every single line of unused code \u003ca href=\"https://github.com/nmlgc/ssg/tree/master/unused\"\u003ein a dedicated directory\u003c/a\u003e, not least because that game has \u003ca href=\"/blog/2022-12-31\"\u003e📝 some very wild effects that should be reasonably preserved\u003c/a\u003e. The problem with this approach is that all this unused code quickly stopped compiling as I started to refactor the game into its current cross-platform state. For discoverability, this is still better than outright deleting the code and expecting people to read pbg's original codebase, but it's not all too practical either. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tIn the ReC98 codebase, we have a different situation: All the unused code doesn't just exist at some old commit that maybe won't even compile going forward, but is an integral part of the \u003ccode\u003emaster\u003c/code\u003e branch. Therefore, removing this code from fork branches is not only in line with their goals, but also completely non-destructive, since its compilable form on \u003ccode\u003emaster\u003c/code\u003e keeps getting maintained \u003ca href=\"https://github.com/nmlgc/ReC98/blob/3130b0ae7125bacb72bfac94ff8873315b87fefa/README.md#supported-build-platforms\"\u003efor a handful of building platforms\u003c/a\u003e.\u003cbr\u003e\n\tThen again, I like the added overview and discoverability of the Shuusou Gyoku approach. So let's meet in the middle: From now on, the \u003ccode\u003edebloated\u003c/code\u003e branch will only keep unused code in the form of its declarations and some short explanatory comments, \u003ca href=\"https://github.com/nmlgc/ReC98/tree/debloated/unused\"\u003ein files within the \u003ccode\u003eunused/\u003c/code\u003e directory\u003c/a\u003e whose names point to the actual implementations on the \u003ccode\u003emaster\u003c/code\u003e branch.\n\u003c/p\u003e\u003cp\u003e\n\tFunnily enough, unused code wasn't even the main reason why TH01's \u003ccode\u003eANNIV.EXE\u003c/code\u003e lost 10,834 bytes between the previous and current builds. Although TH01 is the one game with by far the most unused engine code, that code only made up 3,728 bytes of that difference. The rest came from the work surrounding the zero-copy unpacker and the few portability features that already made sense to be rolled out for this game. Yes, TH01 really is \u003ci\u003ethat\u003c/i\u003e bloated.\n\u003c/p\u003e\u003chr\u003e\u003ch3 id=\"merge-2025-09-29\"\u003eMerging TH02-TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e\u003c/h3\u003e\u003cp\u003e\n\tOnto the second most exciting feature, \u003ca href=\"/blog/2025-05-10#ports-2025-05-10\"\u003e📝 as motivated by the blog post from May\u003c/a\u003e! A true single-executable build \u003ca href=\"/blog/2023-11-30#main-2023-11-30\"\u003e📝 never looked that viable for TH04 and TH05\u003c/a\u003e to begin with, so let's just go for the one viable partial merge that makes sense for all of the four games. With all of \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e being position-independent, the remaining bunch of ASM code there isn't much of an obstacle either.\n\u003c/p\u003e\u003cp id=\"scorefiles-2025-09-29\"\u003e\n\tAnd once again, this merge means that we have to resolve all \u003ca href=\"/blog/2023-03-05#single-2023-03-05\"\u003e📝 binary-specific inconsistencies\u003c/a\u003e at once. While ZUN thankfully eliminated most of them by the end of the PC-98 era, the scorefile code remained inconsistent until the very end, \u003ca href=\"/blog/2025-09-16#score-2025-09-16\"\u003e📝 as Part 3 already mentioned\u003c/a\u003e. Hopefully, this is the second-to-last time I have to mention these formats…\u003cbr\u003e\n\tFunnily enough, all of their most noteworthy inconsistencies are found in how these formats deal with corrupted files:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e The TH03 inconsistency I \u003ca href=\"/blog/2025-09-16#th03-regist-2025-09-16\"\u003e📝 teased in part 3\u003c/a\u003e is almost not worth mentioning. If the game ends up recreating \u003ccode\u003eYUME.NEM\u003c/code\u003e while loading the high scores for name registration after a 1CC, the clear flag is written for all difficulties, not just the one you've actually cleared. Our exact \u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#observable\"\u003edefinition of observable bugs\u003c/a\u003e comes in doubly handy here:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eTo get a 1CC in the first place, you must have gone through character selection, which also (re-)creates \u003ccode\u003eYUME.NEM\u003c/code\u003e if necessary. Therefore, \u003ccode\u003eMAINL.EXE\u003c/code\u003e would only ever recreate \u003ccode\u003eYUME.NEM\u003c/code\u003e in this \"1CC mode\" if something outside the game deleted or tampered with the file while the game was running.\u003c/li\u003e\n\t\u003cli\u003eTH03 offers no benefits for a 1CC on specific difficulties, and doesn't even visually indicate this flag, unlike the three other games. 1CC'ing \u003ci\u003eany\u003c/i\u003e difficulty is all that matters for unlocking Chiyuri and Yumemi.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tWith no way to observe this per-difficulty state, this is one of the rare landmines where we get total freedom for the fix. Thus, we can just do the right thing and set the clear flag for only the current difficulty, reflecting your actual achievements and paving the way for a future feature that can highlight this per-difficulty clear state in the UI.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04's \u003ccode\u003eOP.EXE\u003c/code\u003e simultaneously loads both Reimu's and Marisa's scores for the currently selected difficulty into two separate structures. This alone is a great source of unnecessary inconsistencies, but it gets even worse when either of the two sections is found to be corrupted during decryption. In that case, the game doesn't decrypt Marisa's section and leaves its encrypted state in the respective structure. However, the High Score viewer still assumes that both sections were decrypted. While Reimu's section will always contain either valid or recreated default data, you probably won't see that under all the garbage sprite data rendered for the still encrypted Marisa:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2025-09-29-TH04-High-Score-viewer-Marisa-corruption.webp?63845209\" width=\"640\" alt=\"Screenshot of TH04's High Score viewer, rendering a GENSOU.SCR file whose Marisa/Lunatic section was corrupted with random bytes\"\u003e\n\t\u003cfigcaption\u003eCorruption with random bytes will look slightly more varied than the zeroed-out example from the previous post.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e/\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e The original games would recreate the full \u003ccode\u003eGENSOU.SCR\u003c/code\u003e with its default data if even just one character×difficulty-specific section of the file was found to be corrupted. The debloated build now only resets individual corrupted sections to their default state, preserving as much of the file as possible. This also went hand in hand with removing that separate Marisa score structure in TH04, giving us identical and glitchless corruption repair behavior in both games and saving me from having to mention TH04's corruption behavior in the release notes. Efficiency!\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e/\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e As an added consistency bonus, the debloated builds no longer fully re-encrypt \u003ccode\u003eGENSOU.SCR\u003c/code\u003e after entering a score after a cutscene. \u003ca href=\"https://github.com/nmlgc/ReC98/commit/daf65bcd50e6d89fcb560b1bc0dd3803fd7ce850\"\u003eThis was dumb for many reasons.\u003c/a\u003e\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e Also, they still preserve \u003ca href=\"/blog/2024-12-04#recreate-2024-12-04\"\u003e📝 the inconsistent stage numbers upon recreation\u003c/a\u003e. I couldn't bring myself to fix this 😩\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp id=\"sizes-2025-09-29\"\u003e\n\tThe actual merge then indeed delivers what we were hoping for: In three of the four games, the added unique code from \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e comes in at far below the 20,512 bytes we freed by removing \u003ca href=\"/blog/2023-03-05#single-2023-03-05\"\u003e📝 Borland's C++ exception handler\u003c/a\u003e, both in the binaries themselves and in their loaded in-memory state.\u003cbr\u003e\n\tBut it's TH05 where both \u003ccode\u003eOP.EXE\u003c/code\u003e's expanded Music Room and \u003ccode\u003eMAINE.EXE\u003c/code\u003e's Staff Roll and All Cast sequence add so much unique data that the initial merge ended up slightly larger than the size of the original \u003ccode\u003eMAINE.EXE\u003c/code\u003e. Getting the binary and run-time size of the new \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e below that point required every trick in the book and then some. The more critical tricks were good ideas in their own right:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eHeap-allocating \u003ca href=\"/blog/2020-09-12\"\u003e📝 the scrollable verdict bitmap shown after the Staff Roll\u003c/a\u003e frees up 28,160 bytes of statically allocated memory. The fact that you can just \u003ci\u003ehave\u003c/i\u003e such large arrays of static data seemed like a great benefit of this binary splitting model 5 years ago, but it really doesn't hold up against just writing the two lines to allocate and free that memory from the heap. \u003ccode\u003eMAINE.EXE\u003c/code\u003e's 320,000-byte heap memory limit is more than enough to fit that bitmap in addition to all the simultaneously loaded Staff Roll sprites.\u003c/li\u003e\n\t\u003cli\u003eHeap-allocating \u003ca href=\"/blog/2022-11-30#games-2022-11-30-2022-11-30\"\u003e📝 TH04's and TH05's cutscene script buffer\u003c/a\u003e not only does the same at the smaller scale of 8,192 bytes, but also practically saves over half of that memory, as TH05's largest \u003ci\u003eactual\u003c/i\u003e script (Reimu's Good Ending, stored in \u003ccode\u003e_ED10.TXT\u003c/code\u003e) is just 3,152 bytes. And not just that: It also removes the original 8\u0026nbsp;KiB limit on cutscene scripts in those games, allowing mods to use up to 64\u0026nbsp;KiB just like TH03.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut the rest of them definitely crossed over into silly micro-optimization territory:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\tThe single biggest reduction came from turning the various statically allocated \u003ccode\u003efar\u003c/code\u003e pointers to hardcoded strings into \u003ccode\u003enear\u003c/code\u003e ones. ZUN used the \u003ca href=\"https://en.wikipedia.org/w/index.php?title=X86_memory_models\u0026oldid=1310812895#Memory_models\"\u003eLarge memory model\u003c/a\u003e for every .EXE binary, where every statically initialized C pointer variable not only gets turned into this 4-byte segment+offset form, but also receives a 2-byte relocation in the \u003ca href=\"https://wiki.osdev.org/index.php?title=MZ\u0026oldid=28644#Relocations\"\u003eMZ header\u003c/a\u003e that allows the DOS .EXE loader to adjust the relative segment part to the correct absolute value in conventional RAM. These relocations don't remain in memory after a process has started, but they do have quite an impact on a binary's size if it uses lots of hardcoded strings.\u003cbr\u003e\n\tThe correct high-level solution is to simply switch to the Medium memory model, which restricts a program to just 64\u0026nbsp;KiB of statically allocated data and reduces all data pointers to offset-only \u003ccode\u003enear\u003c/code\u003e pointers by default. Sadly, switching memory models is one of those wide-ranging architectural changes that we absolutely can not realistically do with that much undecompiled and undecompilable ASM left in the codebase:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAll ZUN-written ASM code came out of the disassembler in a Large-exclusive form and would have to be manually adapted to work for the Medium model as well.\u003c/li\u003e\n\t\u003cli\u003eDue to all the code sharing between the games, we'd pretty much have to flip the Medium model switch for all games at the same time. A gradual transition would take even more effort.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tHence, this will only make sense at that far point in the future when we've even translated the majority of undecompilable ASM back to C++. In the meantime, we're left with manually declaring all such pointers as \u003ccode\u003enear\u003c/code\u003e. With a total of 471 pointers to hardcoded strings in the merged TH05 executable, this brought the binary size down by 1,884 bytes. 1,356 of those bytes came from the Music Room and its hardcoded track titles and BGM filenames, but we've also got 300 bytes in \u003ca href=\"/blog/2025-09-16#th05-allcast-2025-09-16\"\u003e📝 the All Cast sequence\u003c/a\u003e, 156 bytes in the main menu, and 72 bytes in the sound setup menu.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tAt startup, Borland's libc must correctly set up buffering for C's \u003ccode\u003estdin\u003c/code\u003e and \u003ccode\u003estdout\u003c/code\u003e streams. Section 7.21.3/7 of the C standard mandates how this setup must behave in case any of these streams are redirected away from the terminal, but even the \u003cq\u003e\"implementation-defined\"\u003c/q\u003e terminal case must at least set up line buffering for \u003ccode\u003estdin\u003c/code\u003e to make \u003ccode\u003escanf()\u003c/code\u003e and similar functions behave as expected, just in case you ever want to use these functions. TH01 uses \u003ccode\u003escanf()\u003c/code\u003e for the stage selection feature in Debug mode, but other games thankfully stay far away from C's standard I/O functions and use master.lib's text layer functions instead. Disabling this I/O setup in the same way we disable Borland's forced C++ exception handler saves 1,722 bytes in TH02-TH05.\u003cbr\u003e\n\tAt least C doesn't even pretend to make you not pay for things you don't use in the way C++ does. It just unconditionally throws \u003ci\u003eall\u003c/i\u003e the trash your way…\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tRemoving trailing whitespace from the hardcoded Music Room track titles and sound setup menu help texts saved another 862 bytes. Hex-editing translators might disapprove, but \u003ci\u003ecome on\u003c/i\u003e, we have C++ code now. If you commit and push your edits somewhere, there's at least a chance that we can keep them working into the future.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe explosion sprite structure in the ZUN Soft logo has an unused 2-byte structure field that wastes 512 statically allocated bytes in the game's data segment. That array would have been another prime candidate for heap allocation, but that would have only been feasible with a decompilation, and \u003ca href=\"/blog/2024-12-04#zunsoft-2024-12-04\"\u003e📝 \u003ci\u003esomeone\u003c/i\u003e insisted on keeping this particular animation in ASM for the time being\u003c/a\u003e…\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tRemoving unnecessary inlining from game startup saved 64 bytes.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tData-driving the Demo Play characters and stages saved 54 bytes.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe original \u003ccode\u003eMAINE.EXE\u003c/code\u003e contains a second copy of the \u003ccode lang=\"ja\"\u003eスローモードでのプレイでは、スコアは記録されません\u003c/code\u003e string because ZUN didn't use a single optimal set of compiler flags for the entire game. Removing that second copy gives us our final 51 bytes.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, another idealistic bonus goal reached! That means we're only missing a single aspect to reach feature parity with the debloated TH01 build:\n\u003c/p\u003e\u003ch3 id=\"game-bat-2025-09-29\"\u003eReplicating TH02-TH05's \u003ccode\u003eGAME.BAT\u003c/code\u003e in C++\u003c/h3\u003e\u003cp\u003e\n\tIn TH03, this is slightly more involved. We not only need to launch PMD using this technique, but also apply it to the \u003ccode lang=\"ja\"\u003eINTvector set program\u003c/code\u003e and SPRITE16. \u003ca href=\"/blog/2023-03-05#mdrv98-2023-03-05\"\u003e📝 You know the way this goes\u003c/a\u003e:\n\u003c/p\u003e\u003cp\u003e\n\t\u003cembed src=\"/blog/static/2025-09-29-TH03-DOS-heap-sketch.svg?45272c1f\" class=\"inline_sprite\" style=\"height: 11.25vw; margin-right: 0.5em; float: left;\"\u003e\n\t…actually, wait a moment! TH02-TH05 don't even use the C heap, and the master.lib heap works in a completely different way. Borland implemented their heap in a traditional \u003ca href=\"https://linux.die.net/man/2/sbrk\"\u003e\u003ccode\u003esbrk(2)\u003c/code\u003e\u003c/a\u003e style that dynamically resizes a program's main DOS memory block as needed, which is how we end up with the whole concept of the heap growing in one direction. master.lib, however, needs to place its heap entirely within a single pre-allocated and fixed-size block of memory. And since \u003ccode\u003emem_assign_dos()\u003c/code\u003e simply allocates this block via the usual \u003ca href=\"https://stanislavs.org/helppc/int_21-48.html\"\u003eDOS \u003ccode\u003eINT 21h, AH=48h\u003c/code\u003e API\u003c/a\u003e, it doesn't matter \u003ci\u003ewhere\u003c/i\u003e on the DOS heap this block is located. This means that we don't even have to do the error-prone witchcraft of pushing these TSRs to the top of conventional memory that we had to do for TH01! Right?\n\u003c/p\u003e\u003cfigure style=\"clear: both;\"\u003e\n\t\u003cembed src=\"/blog/static/2025-09-29-TH03-DOS-heap-naive.svg?d5357a64\"\u003e\n\t\u003cfigcaption\u003eJust like last time, these visualizations are not even remotely to scale.\u003cbr\u003e\n\tAlso, note the small gap between SPRITE16 and PMD. That's \u003ci\u003esupposed\u003c/i\u003e to be PMD's environment segment, and DOS does allocate it, but PMD explicitly frees it before going resident. Sure, it's just a few bytes on real DOS, but still, very considerate!\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tUnfortunately though, the fixed position of all these TSRs would still prevent the \u003cspan style=\"color: red\"\u003egame allocation\u003c/span\u003e from being replaced with a binary that asks for more memory than the one this block was initially allocated for. In TH01, this would have been a minor issue because it only applied to hot-reloading the single \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e or \u003ccode\u003eANNIV.EXE\u003c/code\u003e that contains all game code. For the other four games, however, we still keep the larger \u003ccode\u003eMAIN.EXE\u003c/code\u003e as a separate binary, and most likely will do so for the foreseeable future. And we're surely not getting into the business of \u003cspan class=\"hovertext\" title=\"Seems easy in theory: For TSRs spawned from a .COM binary, their code and data is limited to the single memory block, so we would simply move the memory and adjust the (hardcoded) interrupt vector. But what about TSRs that allocate and save the addresses of other DOS memory blocks (*cough* SPRITE16), or even just they store and load a copy of their their CS/DS/SS register at any globally persistent location? Welcome to replicating tons of implementation details…\"\u003emoving already allocated\u003c/span\u003e TSRs…\u003cbr\u003e\n\tSo we're back to the technique from two years ago after all. Let's precalculate the size of each TSR, push that TSR to the top of conventional RAM by temporarily claiming all free memory minus its expected size, and then we get…\n\u003c/p\u003e\u003cfigure\u003e\u003cembed src=\"/blog/static/2025-09-29-TH03-DOS-heap-ZUN.COM-failure.svg?d3a2ad97\"\u003e\u003c/figure\u003e\u003cp\u003e\n\t…a \u003ccode\u003eZUN.COM\u003c/code\u003e spawn failure from DOS as we try to start the \u003ccode\u003eZUNINIT\u003c/code\u003e sub-binary. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tYup. Thanks to ZUN's fantastic idea of bundling these small utility tools and TSRs into a single binary that's larger than each individual TSR, we can't just reuse the strategy that worked for TH01. DOS must load the entirety of \u003ccode\u003eZUN.COM\u003c/code\u003e into conventional RAM \u003ci\u003ebefore\u003c/i\u003e the bundling code gets a chance to shift the selected sub-binary to the top of the program's memory block and then reduce the size of that block.\u003cbr\u003e\n\tSo how are we going to solve this?\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWe could ship the individual small binaries bundled in \u003ccode\u003eZUN.COM\u003c/code\u003e. But that would defeat the whole point of reducing clutter in the game directory, being even worse than the batch file we're trying to eliminate.\u003c/li\u003e\n\t\u003cli\u003eWe could reserve the entire required size of \u003ccode\u003eZUN.COM\u003c/code\u003e instead of just the size we expect for each TSR. But that would leave the difference between \u003ccode\u003eZUN.COM\u003c/code\u003e and the TSR as an unallocated block we can't do anything with, fragmenting the DOS heap as a result:\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2025-09-29-TH03-DOS-heap-ZUN.COM-overallocated.svg?7191df88\"\u003e\n\t\u003cfigcaption\u003eSure, we could allocate the resident structure into the gap left by \u003ccode\u003eZUNINIT\u003c/code\u003e's instance of \u003ccode\u003eZUN.COM\u003c/code\u003e, but that's just a few bytes. Keep in mind that the \u003ci\u003e(env.)\u003c/i\u003e blocks are much smaller than they appear here. Thus, the free blocks are also a lot larger in reality, but still not large enough to fit PMD and its music buffer.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut if we can't get rid of \u003ccode\u003eZUN.COM\u003c/code\u003e's high load-time memory requirements, how about using that memory more productively? Is there a way we could maybe spawn the other TSRs \u003ci\u003einto\u003c/i\u003e the hole left by \u003ccode\u003eZUN.COM\u003c/code\u003e after it went resident?\u003cbr\u003e\n\tLet's take a step back from individual TSRs and instead look at the full picture of spawning a \u003ci\u003ebundle\u003c/i\u003e of TSRs in a defined order. First, we determine both the \u003ci\u003ebinary size\u003c/i\u003e (file size of the .COM binary + \u003ca href=\"https://en.wikipedia.org/wiki/Program_Segment_Prefix\"\u003eProgram Segment Prefix\u003c/a\u003e + 256 bytes of stack) and the \u003ci\u003eresident size\u003c/i\u003e (the size of its memory block after it goes resident) of each TSR. With these metrics, we can calculate a \u003ci\u003eminimum\u003c/i\u003e and \u003ci\u003eresident\u003c/i\u003e size for the full bundle by simulating the TSR spawns in order:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003euint32_t bundle_size_min = 0;\nuint32_t bundle_size_resident = 0;\n\nfor(const auto\u0026 tsr : tsrs) {\n\t// Since DOS has freed all excess binary memory before we get to spawn a new TSR, the new\n\t// one will end up next to the previous resident allocations. We only need to consider\n\t// the previous minimum size because it might be larger than the one we calculate here.\n\tbundle_size_min = std::max((bundle_size_resident + tsr.size_binary), bundle_size_min);\n\n\tbundle_size_resident += tsr.size_resident;\n}\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tLet's step through the bundle construction for TH03:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers bundle-2025-09-29 naive-2025-09-29\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\u003cth\u003eTSR\u003c/th\u003e\n\t\u003cth\u003eBinary\u003c/th\u003e\n\t\u003cth\u003eResident\u003c/th\u003e\n\t\u003cth\u003eBundle minimum\u003c/th\u003e\n\t\u003cth\u003eBundle resident\u003c/th\u003e\n\t\u003cth\u003eNaive\u003c/th\u003e\n\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003ctd\u003eZUNINIT (\u003ccode\u003eZUN.COM\u003c/code\u003e)\u003c/td\u003e\n\t\u003ctd\u003e23,276\u003c/td\u003e\n\t\u003ctd\u003e 1,056\u003c/td\u003e\n\t\u003ctd\u003e23,276\u003c/td\u003e\n\t\u003ctd\u003e 1,056\u003c/td\u003e\n\t\u003ctd\u003e23,276\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003ctd\u003eSPRITE16 (\u003ccode\u003eZUN.COM\u003c/code\u003e)\u003c/td\u003e\n\t\u003ctd\u003e23,276\u003c/td\u003e\n\t\u003ctd\u003e36,528\u003c/td\u003e\n\t\u003ctd\u003e24,332\u003c/td\u003e\n\t\u003ctd\u003e37,584\u003c/td\u003e\n\t\u003ctd\u003e59,804\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003ctd\u003e\u003ccode\u003ePMD86.COM\u003c/code\u003e\u003c/td\u003e\n\t\u003ctd\u003e29,295\u003c/td\u003e\n\t\u003ctd\u003e30,144\u003c/td\u003e\n\t\u003ctd\u003e66,879\u003c/td\u003e\n\t\u003ctd\u003e67,728\u003c/td\u003e\n\t\u003ctd\u003e89,948\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tThen, we only need to resize our main memory block a single time to leave a gap at the top of conventional RAM whose size matches the larger of the minimum or resident bundle sizes. If we then spawn the TSRs into this gap, we indeed save 22,220\u0026nbsp;bytes over the naive approach! Let's visualize the resulting memory layout with TH02 because there's a nice detail with MMD and PMD:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2025-09-29-TH02-DOS-heap-bundled.svg?b97e37a2\"\u003e\n\t\u003cfigcaption\u003eMMD also frees its environment segment before going resident, so we can subtract the size of one environment segment from the reserved heap size if we spawn both PMD and MMD.\u003cbr\u003e\n\tUnfortunately, \u003ci\u003eone\u003c/i\u003e of these gaps will always remain with this approach. We could get rid of it by spawning MMD and PMD first, which would merge it into the remaining free memory. But then, we'd have nothing to spawn into the hole left by the \u003ccode\u003eZUN.COM\u003c/code\u003e binary for \u003ccode\u003eZUNINIT\u003c/code\u003e, and we'd be back to the naive fragmented situation above. Thus, this trick remains unique to TH02.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHowever, there's one crucial detail in all of this that would prove to be more complicated:\n\u003c/p\u003e\u003ch4 id=\"prespawn-2025-09-29\"\u003eCalculating correct resident sizes\u003c/h4\u003e\u003cp\u003e\n\tIn TH01, this was no big deal. MDRV98 was the only TSR we had to care about, and there was no reason not to just replicate its simple resident size calculation within the code. After all, people would either run the version bundled with the game or the smaller previous version if they played on a real-hardware CanBe model. No one really cares about MDRV98 beyond that level; the driver is almost universally disliked for just not being PMD, which managed to attract a sizable community, documentation, and even \u003ca href=\"http://www5.airnet.ne.jp/kajapon/tool.html\"\u003enew developments\u003c/a\u003e over the years. A PMD port of TH01 has been one of the most common mod requests as well.\u003cbr\u003e\n\tThe TSRs in later games, however, are much more flexible. We compile both \u003ccode\u003eZUNINIT\u003c/code\u003e and \u003ccode\u003eSPRITE16\u003c/code\u003e from source and should therefore expect people to mod them, but these two in particular \u003ci\u003emight\u003c/i\u003e just be considered uninteresting and static enough to justify hardcoding their sizes. But this approach utterly breaks with PMD, whose chip-specific variants come in multiple versions depending on the game:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\" id=\"kaja-2025-09-29\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th02.png?c6bfc44e\" alt=\":th02:\" width=\"24\" height=\"24\" \u003e TH02\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e TH03\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e TH05\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003ePMD.COM\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003ctd colspan=\"4\"\u003e4.8l (1996-12-28)\u003cbr\u003e14,336 bytes\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003ePMDB2.COM\u003c/code\u003e\u003cbr\u003e(ADPCM)\u003c/th\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8l (1996-12-28)\u003cbr\u003e18,496 bytes\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8o (1997-06-19)\u003cbr\u003e18,592 bytes\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003ePMD86.COM\u003c/code\u003e\u003cbr\u003e(86PCM)\u003c/th\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8l (1996-12-28)\u003cbr\u003e19,904 bytes\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8o (1997-06-19)\u003cbr\u003e19,984 bytes\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003ePMDPPZ.COM\u003c/code\u003e\u003cbr\u003e(PPZ8/CanBe)\u003c/th\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8l (1996-12-28)\u003cbr\u003e20,768 bytes\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e4.8o (1997-06-19)\u003cbr\u003e21,024 bytes\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tThe PMD versions that ZUN shipped with each game. The byte size refers to the in-memory TSR size without any music, voice, or effect data added on top.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tIn theory, nothing stops us from hardcoding these sizes for each game as well. But these physical details about specific PMD versions are even less of a property of the game. There's no reason why modders shouldn't be able to replace any of the hardware-specific driver versions with any other – and given the sizable PMD composer and arranger community, this is a much more likely kind of mod to happen. \u003ca href=\"https://www.youtube.com/watch?v=BRy26PViAZs\"\u003eSSG-EG\u003c/a\u003e, anyone?\n\u003c/p\u003e\u003cp\u003e\n\tBut how could we figure out the required resident size of arbitrary PMD versions without hardcoding anything? From the outside, we can only really know for sure by \u003ci\u003erunning\u003c/i\u003e the driver and seeing how much memory it keeps resident…\u003cbr\u003e\n\t…so that's exactly what we need to do. The merged binaries spawn each driver three times during setup – once to figure out its size, a second time to remove this test TSR, and a third time to respawn the TSR at its designated place at the top of conventional memory. And if we have such a system in place, nothing stops us from applying it to all other TSRs as well, removing the need to precalculate or hardcode \u003ci\u003eany\u003c/i\u003e size… well, except for SPRITE16, which \u003ca href=\"https://github.com/nmlgc/ReC98/blob/6729270d15026400ed98643e676be1596a56ea42/th02/pc98/game_bat.cpp#L347-L357\"\u003estill needs a hack to factor in its extra two blocks on the DOS heap\u003c/a\u003e. In TH03, these \u003cspan title=\"Test spawn, test release\"\u003e2\u003c/span\u003e×\u003cspan class=\"hovertext\" title=\"ZUNINIT, SPRITE16, PMD\"\u003e3\u003c/span\u003e additional processes do slow down startup by about 6 frames on our target 66\u0026nbsp;MHz Neko Project configuration when compared to the batch file, which should still be tolerable relative to the .PI load times we removed by switching to PiLoad.\n\u003c/p\u003e\u003cp id=\"obstacles-2025-09-29\"\u003e\n\tThe whole feature has a few other nice properties as well:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\tSince this entire \u003ccode\u003eGAME.BAT\u003c/code\u003e replica should be optional, we need a reliable way of detecting whether we were started from \u003ccode\u003eGAME.BAT\u003c/code\u003e. Checking whether all of a game's TSRs are already resident is the obvious choice here. But then, we can even do one better and only start the \u003ci\u003especific\u003c/i\u003e TSRs that aren't resident by the time our merged binary is started. Of course, removing any non-\u003ccode\u003eZUN.COM\u003c/code\u003e TSR from the bundle will invariably leave gaps in the DOS heap, but we do gain an extra bit of resilience since the game at least \u003ci\u003estarts\u003c/i\u003e in case of a messed-up batch file.\u003cbr\u003e\n\tIf we do see all TSRs in memory though, we also skip TH02's and TH03's bouncing-ball ZUN Soft logo as well as TH05's gaiji upload, \u003ca href=\"/blog/2023-05-01\"\u003e📝 matching the behavior I ended up with in TH01\u003c/a\u003e. After all, we can't validate whether \u003ci\u003ethose\u003c/i\u003e were already run or not. If you remove the \u003ccode\u003ezun -g\u003c/code\u003e line from an edited version of TH05's \u003ccode\u003eGAME.BAT\u003c/code\u003e that launches \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e instead of \u003ccode\u003eOP.EXE\u003c/code\u003e, you'd therefore get the same gaiji- and HUD-less game that you'd get with ZUN's original binaries.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tWe also don't spawn TH04's and TH05's memory checks from C++ for a similar reason. Their hardcoded memory values assume that the checks are run from \u003ccode\u003eGAME.BAT\u003c/code\u003e \u003ci\u003ebefore\u003c/i\u003e the game gets loaded, which would obviously cause them to fail if all menu and cutscene code is already loaded into conventional RAM. After merging that code into a single binary, there's not much of a point to such an upfront check either:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIf there wasn't enough memory to launch \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e/\u003ccode\u003eANNIV.EXE\u003c/code\u003e in the first place, you'd immediately get to know.\u003c/li\u003e\n\t\u003cli\u003eIf the single DOS-heap-allocating call to \u003ccode\u003emem_assign_dos()\u003c/code\u003e failed, we should probably adopt ZUN's original errors to tell you about it in detail, but the game would also refuse to start immediately. This must necessarily be one of the first function calls made by each binary.\u003c/li\u003e\n\t\u003cli\u003eIf either of these two issues occurred for just a game's \u003ccode\u003eMAIN.EXE\u003c/code\u003e, it would be somewhat inconvenient to always go through the title screen animation and the main menu to test any new memory setup, but it wouldn't be a big deal either.\u003c/li\u003e\n\t\u003cli\u003eThe original games did have the theoretical issue that their \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e could have required more memory than either \u003ccode\u003eOP.EXE\u003c/code\u003e or \u003ccode\u003eMAIN.EXE\u003c/code\u003e. Without an upfront check for the expected size of \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e, a lack of memory \u003ci\u003ecould\u003c/i\u003e have meant losing a run to an out-of-memory crash upon switching to \u003ccode\u003eMAINE.EXE\u003c/code\u003e/\u003ccode\u003eMAINL.EXE\u003c/code\u003e, where scores and clear flags get written to disk. In practice, none of the games actually have this issue, and merging the two binaries avoids it entirely.\u003c/li\u003e\n\u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThese merged binaries also integrate PMDPPZ/CanBe support via the \u003ckbd\u003e-c\u003c/kbd\u003e or \u003ckbd\u003e--canbe\u003c/kbd\u003e option. It is quite silly how the community refers to the combination of \u003ccode\u003ePMDPPZ.COM\u003c/code\u003e and \u003ccode\u003eGAMECB.BAT\u003c/code\u003e as a \u003cq\u003eCanBe patch\u003c/q\u003e, since this is a strict surface-level \u003ci\u003eaddition\u003c/i\u003e and doesn't \u003ci\u003emodify\u003c/i\u003e anything. Now that my package integrates at least one of the two required parts, can we maybe stop calling it like that? You even get a nice error message in case \u003ccode\u003ePMDPPZ.COM\u003c/code\u003e is still missing from your game directory!\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tAnd then you test with the actual \u003ccode\u003eZUN.COM\u003c/code\u003e and notice that you're \u003ci\u003estill\u003c/i\u003e not done:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\tThe \u003ccode lang=\"ja\"\u003eINTvector set program\u003c/code\u003e sets up handlers for \u003ccode\u003eINT 5\u003c/code\u003e and \u003ccode\u003eINT 6\u003c/code\u003e, which collide with Turbo C++ 4.0J's implementation of \u003ca href=\"https://man7.org/linux/man-pages/man2/signal.2.html\"\u003e\u003ccode\u003esignal(2)\u003c/code\u003e\u003c/a\u003e. If your program only consists of its main process and the TSR you launch from it, this is no problem as long as you shut down the TSR before your process. However, we want to launch \u003ccode\u003eDEBLOATM.EXE\u003c/code\u003e/\u003ccode\u003eANNIVM.EXE\u003c/code\u003e via \u003ccode\u003eexecl()\u003c/code\u003e from the same process that launched the TSR. You'd think that Borland's \u003ccode\u003esignal()\u003c/code\u003e implementation would then install an \u003ccode\u003eatexit()\u003c/code\u003e handler to restore the specific hooked interrupt vector at shutdown. But no: \u003ccode\u003eexecl()\u003c/code\u003e unconditionally resets \u003ci\u003eall\u003c/i\u003e interrupts that \u003ccode\u003esignal()\u003c/code\u003e can possibly hook to their original handlers during libc initialization, \u003ci\u003eeven if your program never calls \u003ccode\u003esignal()\u003c/code\u003e\u003c/i\u003e. Hence, \u003ccode\u003eexecl()\u003c/code\u003e would not only remove ZUN's \u003ccode\u003eINT 5\u003c/code\u003e and \u003ccode\u003eINT 6\u003c/code\u003e handlers if they were set up by a C++-spawned \u003ccode\u003eZUNINIT\u003c/code\u003e process, but also leak said process: \u003ccode\u003eZUNINIT\u003c/code\u003e's \u003ccode\u003e-r\u003c/code\u003e command locates the resident process via the segment part of the system-wide \u003ccode\u003eINT 6\u003c/code\u003e handler, which obviously no longer works after Borland overwrote that handler.\u003cbr\u003e\n\tThankfully, Borland's function pointers for the original handlers must come with public symbols to remain accessible from two different places in the standard library. Overwriting these pointers after spawning and removing the \u003ccode\u003eZUNINIT\u003c/code\u003e TSR is therefore enough to work around this dumb issue.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tBundling all these small utility programs into \u003ccode\u003eZUN.COM\u003c/code\u003e was apparently not enough for ZUN, and so he additionally compressed TH03's and TH04's \u003ccode\u003eZUN.COM\u003c/code\u003e using Diet. This means that these binaries \u003ci\u003ealso\u003c/i\u003e have to first decompress themselves before they can unbundle and actually launch the requested sub-binary. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e Any compressed binary necessarily decompresses into a process larger than the size of its binary file, and the .COM format has no way of expressing that larger size. Dynamically resizing the program's DOS memory block at startup could work, but Diet made the much more reliable choice of turning such .COM binaries into .EXE binaries, which can declaratively request more memory. Although it certainly is questionable how these binaries retain their original .COM extension… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThus, our TSR size calculation code also needs to support .EXE binaries. The implementation is not complicated at all; you read the MZ header and adapt \u003ca href=\"https://github.com/joncampbell123/dosbox-x/blob/f359dd9a8055c3386868a2ec0dc2c92be851d456/src/dos/dos_execute.cpp#L379-L381\"\u003ethe single expression for calculating the minimum size from DOSBox-X's source code\u003c/a\u003e. But then, we're up for a major disappointment once we see how Diet requests almost one full 64\u0026nbsp;KiB segment to fit both its compressed and decompressed payload. This doesn't matter for TH03, where SPRITE16 allocates an extra 32\u0026nbsp;KB for alpha channels that would be placed into that extra bit of memory allocated for Diet before. But TH04 doesn't have a similarly sized third TSR, which leaves us with an unsightly 34,944-byte hole at the top of the DOS heap:\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003cfigure\u003e\u003crec98-child-switcher\u003e\n\t\u003ctable data-title=\"Uncompressed\" class=\"numbers bundle-2025-09-29\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003eTSR\u003c/th\u003e\n\t\t\u003cth\u003eBinary\u003c/th\u003e\n\t\t\u003cth\u003eResident\u003c/th\u003e\n\t\t\u003cth\u003eBundle minimum\u003c/th\u003e\n\t\t\u003cth\u003eBundle resident\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eZUNINIT (\u003ccode\u003eZUN.COM\u003c/code\u003e)\u003c/td\u003e\n\t\t\u003ctd\u003e13,394\u003c/td\u003e\n\t\t\u003ctd\u003e   784\u003c/td\u003e\n\t\t\u003ctd\u003e13,394\u003c/td\u003e\n\t\t\u003ctd\u003e   784\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003ccode\u003ePMD86.COM\u003c/code\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e29,383\u003c/td\u003e\n\t\t\u003ctd\u003e30,224\u003c/td\u003e\n\t\t\u003ctd\u003e30,167\u003c/td\u003e\n\t\t\u003ctd\u003e31,008\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\t\u003ctable data-title=\"Compressed\" class=\"numbers bundle-2025-09-29 active\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003eTSR\u003c/th\u003e\n\t\t\u003cth\u003eBinary\u003c/th\u003e\n\t\t\u003cth\u003eResident\u003c/th\u003e\n\t\t\u003cth\u003eBundle minimum\u003c/th\u003e\n\t\t\u003cth\u003eBundle resident\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eZUNINIT (\u003ccode\u003eZUN.COM\u003c/code\u003e)\u003c/td\u003e\n\t\t\u003ctd\u003e65,968\u003c/td\u003e\n\t\t\u003ctd\u003e   784\u003c/td\u003e\n\t\t\u003ctd\u003e65,968\u003c/td\u003e\n\t\t\u003ctd\u003e   784\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003ccode\u003ePMD86.COM\u003c/code\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e29,383\u003c/td\u003e\n\t\t\u003ctd\u003e30,224\u003c/td\u003e\n\t\t\u003ctd\u003e66,752\u003c/td\u003e\n\t\t\u003ctd\u003e31,008\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003c/figure\u003e\u003cp\u003e\n\tIt's this TH04 issue that raises the question of whether this whole TSR bundling solution was even worthwhile in the first place. It sure was an interesting problem to solve, but it'd be much simpler \u003ci\u003eand\u003c/i\u003e less bloated to just integrate the \u003ccode lang=\"ja\"\u003eINTvector set program\u003c/code\u003e into every binary. For TH03, we could similarly integrate all SPRITE16 functionality directly into \u003ccode\u003eDEBLOATM.EXE\u003c/code\u003e/\u003ccode\u003eANNIVM.EXE\u003c/code\u003e and \u003ci\u003estill\u003c/i\u003e end up with a smaller-than-original binary after removing Borland's C++ exception handler. That would leave PMD and MMD as the only TSRs we'd need to spawn from C++, and those do have good reasons to be separate from game code.\u003cbr\u003e\n\tOh well, gotta get TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e position-independent first…\n\u003c/p\u003e\u003cp\u003e\n\tAlso, the usual caveats from two years ago still apply. This whole trick of pushing TSRs to the top of conventional RAM still relies on witchcraft that may not work on certain DOS kernels. For developers, tinkerers, and people who know what they're doing, it does succeed at nicely decluttering the game directory. But for… ahem, \u003ci\u003edistributors\u003c/i\u003e, I still recommend shipping the modified version of \u003ccode\u003eGAME.BAT\u003c/code\u003e and \u003ccode\u003eGAMECB.BAT\u003c/code\u003e in the package below to defend against any potential stability issues.\n\u003c/p\u003e\u003chr id=\"th03-vs-quit-2025-09-29\"\u003e\u003cp\u003e\n\tFinally, if the performance improvements aren't enough of a reason to upgrade to these new builds, how about an actual new feature? TH03's Anniversary Edition now lets you quit out of the VS Start menu via either \u003ckbd\u003eESC\u003c/kbd\u003e or a new menu item, without going through the Select screen. 🙌\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2025-09-29-TH03-anniv-VS-Start.webp?38ff11e5\" width=\"640\" alt=\"Screenshot of the VS Start menu in the P0323 build of TH03's Anniversary Edition, showing off the new Quit option and the version text\"\u003e\n\t\u003cfigcaption\u003eMatching the style of the version text to the style of \u003cq\u003e☪ The Phantasmagoria of Dim. Dream\u003c/q\u003e on the other side seemed like the least bad option here. That outline is indeed created by rendering every line 9 times…\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd with that, I'm finally done with 2025's most indulgent subproject! Let's quickly check the overall impact on the codebase:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e$ git diff --stat debloated~193 debloated -- . \":(exclude)Tupfile.lua\" \":(exclude)build_dumb.bat\" \":(exclude)unused/\"\n[…]\n259 files changed, 4145 insertions(+), 8099 deletions(-)\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tThat's almost 4,000 lines of ad-hoc PC-98-native graphics code, bloat, landmines, bloat- and landmine-documenting comments, and binary-specific inconsistencies removed from game code, in exchange for…\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003egit diff --stat master~203 master -- platform/\n[…]\n28 files changed, 2213 insertions(+), 258 deletions(-)\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\t…not even half that many additional lines in the platform layer. And here's what all of this compiles to:\n\u003c/p\u003e\u003cp\u003e\n\t\t\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ReC98/releases/tag/P0323\"\u003e\n\t\t\t\t\u003cimg\n\t\t\t\t\tsrc=\"/static/logo.png?af58e856\"\n\t\t\t\t\talt=\"Richard Stallman cosplaying as a shrine maiden\"\n\t\t\t\t\tclass=\"inline_sprite\"\n\t\t\t\t\twidth=\"24\"\n\t\t\t\t\theight=\"24\"\n\t\t\t\t\u003e ReC98 (version P0323)\n\t\t\t\u003c/a\u003e\n\t\t\t\u003ca class=\"download\" href=\"/blog/static/2025-09-29-ReC98.zip?cfec4d37\" data-kb=\"740.3\"\u003e2025-09-29-ReC98.zip \u003c/a\u003e\n\t\t\u003c/p\u003e\u003cp\u003e\n\tAfter the Shuusou Gyoku debacle and the many last-minute fixes that cropped up while I was writing this post, I'm not particularly confident in these builds, despite the weeks of testing that went into them. Still, we've got to start \u003ci\u003esomewhere\u003c/i\u003e. At least for TH03, we're bound to quickly find any issues that slipped through the cracks while I'm implementing netplay into the Anniversary Edition.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The very quick round of \u003ca href=\"/blog/2025-04-09#tag-2025-04-09\"\u003e📝 Shuusou Gyoku maintenance and forward compatibility\u003c/a\u003e I announced in April, to clear out the backlog a bit. This whole series also really stretched the concept of what 11 pushes should be, so I'll charge 2 pushes for that maintenance round to compensate. In exchange, I'll also incorporate a small bit of new Windows 98 feature work, since it fits nicely with the cleanup work.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-10-19\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-09-29T23:55:58Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-09-16",
      "url": "https://rec98.nmlgc.net/blog/2025-09-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-09-16\"\u003e\u003ctime datetime=\"2025-09-16T22:21:31Z\"\u003e2025-09-16 22:21\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0317\"\u003eP0317\u003c/a\u003e\n\t\t\tTH02 RE (Main menu overhaul) / TH03 decompilation (High score menu, part 2/2) / TH02/TH03 debloating (Initial screen tearing fixes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9e38693...504d677\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e1270ee...6734fa1\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0318\"\u003eP0318\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (TH04 title screen animation / TH05 All Cast sequence / GENSOU.SCR, part 3/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/504d677...b2c520b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous], Yanga, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gaiji\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bgm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The music behind Touhou. Arguably the core of what motivated ZUN to create this series to begin with.\"\u003ebgm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\u003cp\u003e\n\tPart 3 of \u003ca href=\"/blog/2025-09-06\"\u003e📝 the 4-post series about the big 2025 PC-98 Touhou portability subproject\u003c/a\u003e, and we actually get to move some percentages on the front page with this one! For once, there truly isn't a lot to mention about most of these five disconnected small-feature decompilations, so let's go for more of a \u003ca href=\"https://touhou-memories.com\"\u003etouhou-memories\u003c/a\u003e style and string together a few shorter bullet points and paragraphs. For even greater brevity, I'll also use the ZUN code issue emoji you might already know from Twitter or Bluesky: 🐞 denotes a bug, 💣 denotes a landmine, and 🎺 denotes a quirk.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#th02-main-2025-09-16\"\u003eRevising TH02's main menu\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th03-regist-2025-09-16\"\u003eFinishing TH03's High Score menu\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th04-title-2025-09-16\"\u003eTH04's title screen animation\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th05-allcast-2025-09-16\"\u003eTH05's All Cast sequence\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#score-2025-09-16\"\u003eFinishing TH03/TH04/TH05 scorefiles\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th04-script-bug-2025-09-16\"\u003eA typo in TH04 Reimu A's Good Ending\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"th02-main-2025-09-16\"\u003e\u003ch3\u003eRevising TH02's main menu\u003c/h3\u003e\u003cp\u003e\n\tThis was one of those old decompilations from 2015 that I really wanted to bring up to current standards before the \u003ccode\u003edebloated\u003c/code\u003e branch would roll out the new more portable and performant blitting code. Replacing the magic-number coordinates with constants and calculations revealed \u003ca href=\"/blog/2025-05-10#menu-2025-05-10\"\u003e📝 the usual off-by-one text positioning bugs in the Option menu\u003c/a\u003e, despite ZUN still using monospaced text in this game…\u003cbr\u003e\n\tAs for more unique and exciting details in this screen: ZUN's defined \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e strings contain an unused adaptation of TH01's blinking \u003ccode lang=\"ja\"\u003eＨＩＴ　ＫＥＹ\u003c/code\u003e text. On screen, it might have looked something like this:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-09-16-TH02-Hit-key-mockup.webp?5edca87c\"\n\t\twidth=\"640\"\n\t\talt=\"Mockup of the TH01's \u0026amp;ＨＩＴ　ＫＥＹ\u0026amp; string in TH02\"\n\t\u003e\n\t\u003cfigcaption\u003e\n\t\tThis string is so unused that we don't even know its intended position, though.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"th03-regist-2025-09-16\"\u003e\u003ch3\u003eFinishing TH03's High Score menu\u003c/h3\u003e\u003cp\u003e\n\tAt the end of 2021, \u003ca href=\"/blog/2021-12-27\"\u003e📝 I already decompiled most of this menu\u003c/a\u003e, but left two functions in ASM due to push scope constraints. Originally, I thought that this menu would need a few changes to address a certain scorefile inconsistency I'll mention in Part 4, but I ended up finding a better solution. Still, we got one interesting discovery per function out of it:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tIf you've ever entered a score and were too lazy to type a proper name, you know that TH03 just uses the name of the player character in Romaji if you enter either nothing or \u003ckbd\u003eAAAAAAAA\u003c/kbd\u003e. But did you know that this happens if you enter \u003ci\u003eany\u003c/i\u003e letter 8 times?\n\t\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-16-TH03-High-Score-default-name.webp?4f11c998\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"376\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-16-TH03-High-Score-default-name.avi?cae94367\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-16-TH03-High-Score-default-name.webm?990c0a80\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-16-TH03-High-Score-default-name.webm?48cca4c1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-16-TH03-High-Score-default-name.webm?2c118445\" type=\"video/webm\"\u003eVideo showcasing the default name replacement in TH03's High Score menu, triggered by entering BBBBBBBB to showcase that this happens for any character, not just A or an empty name. \u003ca href=\"/blog/static/video/zmbv/2025-09-16-TH03-High-Score-default-name.avi?cae94367\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\n\u003c/li\u003e\u003cli\u003e\n\t🐞 When sorting a new score into the list, ZUN does not look at the 9\u003csup\u003eth\u003c/sup\u003e digit, i.e., the number of continues used. If you ever manage to enter a score whose most significant 8 digits match an existing entry in the current difficulty's score list, those two scores are considered equal and the new score always gets inserted below the old one. If you enter more than one such score, the list will therefore maintain the order in which the scores were entered:\n\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2025-09-16-TH03-High-Score-credit-sort-bug.webp?3a1e0c0d\"\n\t\t\twidth=\"640\"\n\t\t\talt=\"Screenshot of TH03's sorting bug for new high scores. Places 2 to 5 show four 800-million scores that used 0, 3, 1, and 2 continues\"\n\t\t\u003e\n\t\t\u003cfigcaption\u003eIn this example, I first entered 800-million scores with 0, 3, and 1 continues in exactly this order, before entering this new 2-continue score.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003chr id=\"th04-title-2025-09-16\"\u003e\u003ch3\u003eTH04's title screen animation\u003c/h3\u003e\u003cp\u003e\n\tThis decompilation was necessary because its palette manipulation code did the very dubious thing of accessing the palette in a freed .PI slot. I don't think that the stylish effect of separately whiting in the image's black outlines is appreciated enough. And yes, that formally was the last non-RE'd tiny bit of any \u003ccode\u003eOP.EXE\u003c/code\u003e binary!\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-16-TH04-Title-animation.webp?4f11c998\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"211\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-16-TH04-Title-animation.avi?7d6736c9\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-16-TH04-Title-animation.webm?d7794a22\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-16-TH04-Title-animation.webm?fab89ec9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-16-TH04-Title-animation.webm?2fb1d535\" type=\"video/webm\"\u003eVideo of TH04's title animation. \u003ca href=\"/blog/static/video/zmbv/2025-09-16-TH04-Title-animation.avi?7d6736c9\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"90\" data-title=\"Outline fade-in start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"106\" data-title=\"Outline fade-in end\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tAlso note that single black pixel in Reimu's gohei. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"th05-allcast-2025-09-16\"\u003e\u003ch3\u003eTH05's All Cast sequence\u003c/h3\u003e\u003cp\u003e\n\tThis sequence contained the last not yet decompiled instance of \u003ca href=\"/blog/2025-09-10#xfade-2025-09-10\"\u003e📝 masked crossfading\u003c/a\u003e, which the \u003ccode\u003edebloated\u003c/code\u003e branch wants to replace with our single optimized implementation.\u003cbr\u003e\n\tMost picture and text cues in this sequence are synced to the BGM, using \u003ca href=\"https://github.com/nmlgc/ReC98/blob/9e3869341542d9fa8220056c972bf709a989bd54/libs/kaja/pmddata.doc#L172-L184\"\u003ePMD's \u003ccode\u003eAH=05h\u003c/code\u003e function to retrieve the current measure\u003c/a\u003e. And yes, that's \u003ci\u003emeasures\u003c/i\u003e, which is indeed the only time unit you get from PMD. The cues appear to be timed based on \u003ci\u003ebeats\u003c/i\u003e rather than measures, but the secret there is that ZUN simply wrote \u003cspan lang=\"ja\"\u003ePeaceful Romancer\u003c/span\u003e in the internal time signature of \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e4\u003c/sub\u003e. Just in case anyone tries to mod this BGM and starts wondering why the sequence suddenly progresses more slowly. I'll just use \u003cq\u003ebeats\u003c/q\u003e below since it's shorter.\u003cbr\u003e\n\tAny cues that \u003ci\u003edon't\u003c/i\u003e appear synced only do so because of – you guessed it – weird ZUN code issues.\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003e🐞 But first, what happens if you run the game on a system without an FM chip? PMD does remain resident in that case, but enters a reduced-functionality mode that refuses to even process song data, leaving you with no BGM beats to sync to. Due to the various ways of setting the tempo in a .M file, it's impossible to just parse out the tempo without \u003ca href=\"https://github.com/Blargzargo/pmd2mml\"\u003ereimplementing the entire format\u003c/a\u003e, so it makes sense why ZUN just hardcoded a fixed replacement delay of 44 frames per beat. However, 44 frames translate to (\u003csup\u003e44\u003c/sup\u003e/\u003csub\u003e56.423\u003c/sub\u003e)\u0026nbsp;≈ 780\u0026nbsp;ms\u0026nbsp;≈ 76.94\u0026nbsp;BPM, which is ~1.9× slower than \u003cspan lang=\"ja\"\u003ePeaceful Romancer\u003c/span\u003e's actual ~145-147 BPM.\u003cbr\u003e\n\tDiscoveries like these always start out as quirks until I find evidence that would promote them to bugs. And sure enough: ZUN renders this entire sequence at the halved frame rate of 28.212\u0026nbsp;FPS, that slowdown factor is suspiciously close to 2, and \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th05/end/allcast.cpp#L140\"\u003ethe code actually specifies 22 frames\u003c/a\u003e. This looks as if ZUN simply didn't realize that 22 frames would only translate to the slightly more correct 153.88\u0026nbsp;BPM at the native frame rate of 56.423\u0026nbsp;FPS.\u003cbr\u003e\n\tThis bug also applies if you deactivated BGM in the Option menu, since ZUN treats both cases identically.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003e🎺 The very first crossfading animation doesn't appear to be synced to any beat, though? It starts close to but not exactly on beat 5:\u003c/p\u003e\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-16-TH05-All-Cast-first-original.webp?4f11c998\" preload=\"none\" controls data-title=\"Original game\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"186\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-first-original.avi?49f80303\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-16-TH05-All-Cast-first-original.webm?84d78f0d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-16-TH05-All-Cast-first-original.webm?256f42f6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-16-TH05-All-Cast-first-original.webm?f373670f\" type=\"video/webm\"\u003eVideo of the first 8 beats of TH05's All Cast sequence as shown in the original game, showcasing how 1) the first picture crossfades onto the screen way behind the second beat of Peaceful Romancer indicated in the code, and 2) how the crossfading effect only uses two out of the four 4×4 mask patterns. \u003ca href=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-first-original.avi?49f80303\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"23\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"47\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"70\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"94\" data-title=\"5\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"117\" data-title=\"6\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"141\" data-title=\"7\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"164\" data-title=\"8\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-16-TH05-All-Cast-first-dequirked.webp?4f11c998\" preload=\"none\" controls data-title=\"Dequirked version\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"186\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-first-dequirked.avi?7f45ac5c\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-16-TH05-All-Cast-first-dequirked.webm?1a7fb7b6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-16-TH05-All-Cast-first-dequirked.webm?5613e869\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-16-TH05-All-Cast-first-dequirked.webm?e4e1faf0\" type=\"video/webm\"\u003eVideo of the first 8 beats of TH05's All Cast sequence in a hypothetical dequirked version that 1) crossfades the first picture exactly on the second beat of Peaceful Romancer as indicated in the code, and 2) uses all four 4×4 mask patterns. \u003ca href=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-first-dequirked.avi?7f45ac5c\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"23\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"47\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"70\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"94\" data-title=\"5\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"117\" data-title=\"6\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"141\" data-title=\"7\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"164\" data-title=\"8\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003c/figure\u003e\n\t\u003cp\u003eThis one is quickly explained: ZUN does \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th05/end/allcast.cpp#L176\"\u003eenter the first screen within 2 frames of \u003cspan lang=\"ja\"\u003ePeaceful Romancer\u003c/span\u003e's first downbeat on \"beat\" 3\u003c/a\u003e, but each screen \u003ci\u003eactually\u003c/i\u003e starts with a 34-frame fade-out of the previous screen before crossfading in the new picture. Hence, most of this apparent delay is taken up by a fade-out from black to black. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e The remaining 4 frames between the beat and the first visible on-screen pixels can be attributed to double-buffering at the sequence's halved frame rate.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003e🎺 Also, why does the crossfading animation only use two of the four mask patterns across its 16 frames? This seems like \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th05/end/allcast.cpp#L91\"\u003ea typo in the code\u003c/a\u003e, but was almost certainly done on purpose to make this sequence feel more languid and relaxed. The dequirked version with all four mask patterns looks almost too hectic, especially compared to the single mask pattern that ZUN used for text.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eBut even after that initial screen, the first two or three text cues on later screens don't appear in sync with the BGM beats either?\u003c/p\u003e\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-16-TH05-All-Cast-catch-up.webp?4a33b2ef\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"374\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-catch-up.avi?cc7047ed\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-16-TH05-All-Cast-catch-up.webm?d595141a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-16-TH05-All-Cast-catch-up.webm?201fc007\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-16-TH05-All-Cast-catch-up.webm?59a03a98\" type=\"video/webm\"\u003eVideo of the transition from the second to the third screen of Yuuka's TH05 All Cast sequence, demonstrating how the picture and text cues have to catch up with the BGM beat targets during such a transition. \u003ca href=\"/blog/static/video/zmbv/2025-09-16-TH05-All-Cast-catch-up.avi?cc7047ed\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"40\" data-title=\"Fade-out\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"77\" data-title=\"Fade-in\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"120\" data-title=\"TH02\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"142\" data-title=\"Reimu\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"180\" data-title=\"Rika\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"228\" data-title=\"Meira\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"274\" data-title=\"Marisa\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"322\" data-title=\"Mima\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eAs pointed out by the uneven placement of the Reimu and Rika cues.\u003cbr\u003e\n\t\tThese are Yuuka's second and third screens; the fact that each character gets its own sequence of pictures is common knowledge by now, right?\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cp\u003e\n\t\tTo understand this, we have to look at how ZUN defines the target BGM beat for each cue in the first place. There's only a single variable that defines the target beat for the BGM-syncing delay, and ZUN simply adds a certain number of beats to this variable before every cue. In the case of these text cues, he adds 2 beats, which matches what we can observe for the correctly synced cues in the video above. The very first text cue, however, is placed two beats after… the beat the fade-out was started on, even though we've just spent at least 56 frames on the two fading effects. This means that BGM playback will not only have already reached this beat, but will even have progressed about half a beat beyond. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Thus, the game just fades in the text immediately…\n\t\u003c/p\u003e\u003cp\u003e\n\t\t💣 …except that it doesn't! All of the above was pretty quirky, but then ZUN adds a definite landmine by loading the .PI file with the picture for the next screen \u003ci\u003eright after the fade-in animation\u003c/i\u003e. If you just look at \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th05/end/allcast.cpp#L113-L117\"\u003ethe few lines after that load call\u003c/a\u003e, this seems like a productive use of an intended 2-beat delay, but we don't actually get that 2-beat delay, as I explained above. Instead, BGM playback gets to progress even further beyond the target beat, by the CPU-specific amount of frames it takes to load that next .PI image on the system the game happens to run on. I've recorded the video above by running the original game on our target Neko Project 66\u0026nbsp;MHz configuration, and got an additional 17 frames of cue drift, between frames 101 and 118 inclusive. In the end, it takes the first three text cues for the beat target to catch up with the BGM on this system, and we only return to proper syncing with Meira, where the beat target has finally moved ahead of BGM playback.\u003cbr\u003e\n\t\tThat .PI load call would have been much more appropriate before the 30-beat delay in front of the fade-out…\n\t\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003e💣 Even worse, ZUN also loads a new image on the last screen, which defines no next image. This causes the game to unconditionally load from a null pointer, resulting in a landmine in \u003ca href=\"/blog/2023-03-05#landmines-2023-03-05\"\u003e📝 the classic sense of the word\u003c/a\u003e: You can completely ignore it on PC-98 because\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eReal Mode just lets you read from address \u003ccode\u003e0000:0000\u003c/code\u003e without a segmentation fault\u003c/li\u003e\n\t\t\u003cli\u003eThe far pointer to the handler for \u003ccode\u003eINT 0\u003c/code\u003e is highly unlikely to actually point to the name of an existing file\u003c/li\u003e\n\t\t\u003cli\u003eThat file is even less likely to be a valid .PI file\u003c/li\u003e\n\t\t\u003cli\u003eThe game won't display that image anyway, and free its buffer once the sequence ends shortly after\u003c/li\u003e\n\t\u003c/ul\u003eBut you wouldn't want to rely on null pointers being filtered by the platform layer.\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"score-2025-09-16\"\u003e\u003ch3\u003eFinishing TH03/TH04/TH05 scorefiles\u003c/h3\u003e\u003cp\u003e\n\tWell, at least as far as decompilation is concerned. Cleaning up all these binary-specific inconsistencies on the \u003ccode\u003edebloated\u003c/code\u003e branch will be just as annoying as reconstructing them in the first place, and I won't even get it all the way done within these 11 pushes. TH05 made this even worse by continuing its general trend of taking TH04's slightly bloated but overall fine C++ code and needlessly rewriting it in micro-optimized and only semi-decompilable ASM. If you still believe that the \u003ccode\u003emaster\u003c/code\u003e branch is a good foundation for any kind of serious work, \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th04/hiscore/score_ld.cpp\"\u003ethis file\u003c/a\u003e should convince you otherwise.\u003cbr\u003e\n\tTwo more discoveries here:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003eIf you game over and continue in-game while having a score that would qualify for the current character/difficulty list, the game automatically enters it with a \u003ccode\u003eCONTINUE\u003c/code\u003e name while staying within \u003ccode\u003eMAIN.EXE\u003c/code\u003e. Of course, this means that both games get yet another dedicated piece of code to mutate the High Score list… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\t🐞 And so, the TH04 variant of this code also gets its own distinct version of the \u003ca href=\"/blog/2024-12-04#limit-2024-12-04\"\u003e📝 C integer promotion issue\u003c/a\u003e that limits the technically supported score to 959 million points. In an unexpected twist though, TH05's ASM rewrite actually manages to fix this issue in a surprisingly \u003cq\u003enatural\u003c/q\u003e way by explicitly performing the necessary calculations on 8-bit registers. On the other hand, fixing it within C++ would have still been totally possible \u003ci\u003eand\u003c/i\u003e natural \u003ci\u003eand\u003c/i\u003e code-simplifying…\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe single biggest source of inconsistencies can be found in the code that recreates corrupted scorefiles. During my tests of the cleaned-up and improved rewrite on the \u003ccode\u003edebloated\u003c/code\u003e branch, I regularly had to corrupt these files on purpose. File contents getting fully or partially overwritten with \u003ccode\u003e00\u003c/code\u003e bytes is the most common kind of corruption you'd encounter with modern operating systems and SSDs, but hilariously enough, that happens to be the exact kind of corruption these games might even fail to detect. If these \u003ccode\u003e00\u003c/code\u003e bytes cover an \u003ci\u003eentire\u003c/i\u003e character-/difficulty-specific section, all three games consider such a zeroed section as valid, since it passes checksum validation? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tThe deobfuscation algorithm explains why:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e// [key1] and [key2] are `uint8_t` as well.\ndecoded_byte[i] = (key1 + (std::rotr\u0026lt;uint8_t\u0026gt;(encoded_byte[i + 1], 3) ^ key2) + encoded_byte[i]);\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tWhen saving a section within these files, the games generate new random values for \u003ccode\u003ekey1\u003c/code\u003e and \u003ccode\u003ekey2\u003c/code\u003e and store them directly in the file. Without any kind of hardcoded nonce to perturb the input, this obfuscation scheme thus fully relies on the combination of keys and data to generate random-looking output. Set both of them to 0, and deobfuscation turns into a no-op. Then, a buffer of \u003ccode\u003e00\u003c/code\u003e also sums to 0, which also matches the 0 checksum in the file. In contrast, \u003ca href=\"https://github.com/nmlgc/ReC98/blob/b2c520bd4eb3f9a0befa2653f765b802f3d948ae/th02/scoreenc.c#L19\"\u003eTH02's obfuscation scheme\u003c/a\u003e lacked any source of randomness, but it did cover this exact case… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tHere's how such a fully zeroed-out \u003ccode\u003eGENSOU.SCR\u003c/code\u003e looks like in TH04's and TH05's High Score viewer:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\u003crec98-child-switcher\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-09-16-TH04-0-byte-GENSOU.SCR.webp?71a47328\"\n\t\tdata-title=\"TH04\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH04's High Score viewer, rendering a GENSOU.SCR file that consists entirely of 00 bytes\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-16-TH05-0-byte-GENSOU.SCR.webp?d2afd451\"\n\t\tdata-title=\"TH05\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH05's High Score viewer, rendering a GENSOU.SCR file that consists entirely of 00 bytes\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf you remember how \u003ccode\u003eGENSOU.SCR\u003c/code\u003e saves scores in \u003ca href=\"/blog/2024-12-04#limit-2024-12-04\"\u003e📝 this silly gaiji-offsetted way\u003c/a\u003e, these screens almost explain themselves. 0 minus 160 will always be an invalid sprite ID, and since master.lib's \u003ccode\u003esuper_put()\u003c/code\u003e doesn't bounds-check sprite IDs, it blindly accesses invalid sprite data and probably ends up filling every VRAM bitplane with 1 bits. After the game spent way too much time rendering this garbage data, we then only end up seeing the sprites that get rendered after the very last score digit.\u003cbr\u003e\n\tThe \u003cspan style=\"color: #f44\"\u003eVV\u003c/span\u003e characters might look especially weird in place of the usual stage number, but they quickly make sense once you remember that these numbers are gaiji rendered to VRAM. The PC-98's character generator simply can't support a gaiji with an ID of 0, since it would have to be encoded as \u003ccode\u003e0x\u003cstrong\u003e00\u003c/strong\u003e56\u003c/code\u003e, which is indistinguishable from the halfwidth V in ASCII. And since master.lib assumes that all gaiji are fullwidth, we get two of them next to each other.\n\u003c/p\u003e\u003cp\u003e\n\tThe visual result for a zeroed-out \u003ccode\u003eYUME.NEM\u003c/code\u003e in TH03's High Score screen, however, is much more… well-defined:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-09-16-TH03-0-byte-YUME.NEM.webp?ab49d5e2\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH03's High Score viewer, rendering a YUME.NEM file that consists entirely of 00 bytes\"\n\t\u003e\n\t\u003cfigcaption\u003e\n\t\tSince \u003ccode\u003eYUME.NEM\u003c/code\u003e stores names, scores, and stage numbers as raw sprite IDs, we get sprite #0 from \u003ccode\u003eREGI2.BFT\u003c/code\u003e for all of them.\u003cbr\u003e\n\t\tAAAAAAAA AAAAAAAAAA A\u003c/figcaption\u003e\n\u003c/figure\u003e\u003c/li\u003e\u003c/ul\u003e\u003chr id=\"th04-script-bug-2025-09-16\"\u003e\u003cp\u003e\n\tFinally, I stumbled over a script bug in TH04's Good Ending for Reimu A:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-09-16-TH04-Reimu-A-Good-Ending-script-bug.webp?5858d9e7\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the \u0026amp;,4魔理沙\u0026amp; script bug in TH04's Good Ending for Reimu A\"\n\t\u003e\u003cfigcaption\u003e\n\t\tThe 2014 static English patch fixes this issue. That's probably why this isn't talked about anywhere.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis looks unintentional, and the same line in Reimu B's Good Ending confirms that this is indeed a typo:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher class=\"plaintext\"\u003e\n\t\u003cblockquote class=\"active\" data-title=\"\u003ccode\u003e_ED000.TXT\u003c/code\u003e\"\n\u003e\\p,ed07.pi\n\\=0,4\n\u003cspan class=\"ja\"\u003e魔理沙：なんだよ、そりゃ\u003c/span\u003e\\ga9\\s160\\c\u003c/blockquote\u003e\u003cblockquote\n\tdata-title=\"\u003ccode\u003e_ED010.TXT\u003c/code\u003e\"\n\u003e\\p,ed07.pi\n\\==0,4\n\u003cspan class=\"ja\"\u003e魔理沙：なんだよ、そりゃ\u003c/span\u003e\\ga9\\s160\\c\u003c/blockquote\u003e\n\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe \u003ca href=\"/blog/2022-11-30#ref-2022-11-30\"\u003e📝 cutscene command reference\u003c/a\u003e tells us that the line in the Reimu B variant is preceded by \u003ccode\u003e\\==\u003c/code\u003e, the picture crossfading command, followed by both possible parameters, \u003cvar\u003e0\u003c/var\u003e and \u003cvar\u003e4\u003c/var\u003e. Reimu A's script, however, lacks that second \u003ccode\u003e=\u003c/code\u003e and instead spells out \u003ccode\u003e\\=\u003c/code\u003e, the immediate picture display command, which doesn't take a second parameter. Thus, the command stops reading after the \u003cvar\u003e0\u003c/var\u003e and leaves the trailing \u003ccode\u003e,4\u003c/code\u003e as text to be displayed in the newly started box. The line break is then ignored as usual, causing \u003ccode lang=\"ja\"\u003e魔理沙\u003c/code\u003e to be displayed right next to these two characters.\n\u003c/p\u003e\u003cp\u003e\n\tWhew! Once again, this did turn into more of the typical ReC98 research by the end after all. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e And that was just 75% of the pushes assigned to this post, because the rest already went towards the debloating work. Next up: Concluding this series and actually applying all this research to the games.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-09-16T22:21:31Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-09-10",
      "url": "https://rec98.nmlgc.net/blog/2025-09-10",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-09-10\"\u003e\u003ctime datetime=\"2025-09-10T23:56:09Z\"\u003e2025-09-10 23:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0314\"\u003eP0314\u003c/a\u003e\n\t\t\tResearch (Common blitting benchmark framework / Blitting performance of wide sprites)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/04724c9...e44e876\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0315\"\u003eP0315\u003c/a\u003e\n\t\t\tDebloating (Loading .PI images into planar buffers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e44e876...c8e3189\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0316\"\u003eP0316\u003c/a\u003e\n\t\t\tResearch (Crossfading benchmark) / Portability (Point, surface, and VBLANK abstractions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c8e3189...9e38693\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Congrio, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cstyle\u003e\n\t.cycles-2025-09-10 tbody tr th {\n\t\ttext-align: left;\n\t}\n\n\t.benchmark-2025-09-10 \u003e div {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: min-content 7em;\n\t\tplace-items: center;\n\t}\n\n\t.benchmark-2025-09-10 table {\n\t\tfont-size: min(2.25vw, 100%);\n\t}\n\n\t.benchmark-2025-09-10 table .used {\n\t\tfont-weight: 900;\n\t}\n\n\t.benchmark-2025-09-10 table.wide th:nth-child(2),\n\t.benchmark-2025-09-10 table.wide td:nth-child(2) {\n\t\tcolor: #ffb120\n\t}\n\n\t.benchmark-2025-09-10 table.wide th:nth-child(3),\n\t.benchmark-2025-09-10 table.wide td:nth-child(3) {\n\t\tcolor: #f65a3b\n\t}\n\n\t.benchmark-2025-09-10 table.wide th:nth-child(4),\n\t.benchmark-2025-09-10 table.wide td:nth-child(4) {\n\t\twidth: 5ch;\n\t\tcolor: #efee1d\n\t}\n\n\t.benchmark-2025-09-10 table.wide th:nth-child(5),\n\t.benchmark-2025-09-10 table.wide td:nth-child(5) {\n\t\tcolor: #98d65b\n\t}\n\n\t.benchmark-2025-09-10 table.xfade th:nth-child(2),\n\t.benchmark-2025-09-10 table.xfade td:nth-child(2) {\n\t\tcolor: #98d65b\n\t}\n\n\t.benchmark-2025-09-10 table.xfade th:nth-child(3),\n\t.benchmark-2025-09-10 table.xfade td:nth-child(3) {\n\t\tcolor: #efee1d\n\t}\n\n\t.benchmark-2025-09-10 table.xfade th:nth-child(4),\n\t.benchmark-2025-09-10 table.xfade td:nth-child(4) {\n\t\tcolor: #f65a3b\n\t}\n\n\t.benchmark-2025-09-10 table.xfade th:nth-child(5),\n\t.benchmark-2025-09-10 table.xfade td:nth-child(5) {\n\t\twidth: 5ch;\n\t\tcolor: #ffb120\n\t}\n\n\t.benchmark-2025-09-10 table tr {\n\t\tborder-bottom: var(--table-border);\n\t}\n\t.benchmark-2025-09-10 table th:not(:last-child),\n\t.benchmark-2025-09-10 table tr:first-child td:not(:last-child),\n\t.benchmark-2025-09-10 table tr:not(:first-child) td {\n\t\tborder-right: var(--table-border);\n\t}\n\n\t.benchmark-2025-09-10 embed {\n\t\theight: 28lh;\n\t\tmax-height: 28lh;\n\t}\n\n\t.pi-2025-09-10 table td:not(:first-child) {\n\t\twidth: 11ch;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tPC-98 blitting performance research, part 2! As well as part 2 in \u003ca href=\"/blog/2025-09-06\"\u003e📝 the 4-post series about the big 2025 PC-98 Touhou portability subproject\u003c/a\u003e. This one gets pretty technical and is primarily interesting for anyone else wanting to write code for the PC-98. If you're just here for funny details about PC-98 Touhou, you can \u003ca href=\"/blog/2025-09-10#pi-th03-2025-09-10\"\u003e📝 skip to the last two bullet points\u003c/a\u003e.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#wide-2025-09-10\"\u003eBenchmarking wide and opaque sprites\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#wide-algs-2025-09-10\"\u003eAlgorithms\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#wide-res-2025-09-10\"\u003eResults\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#blitter-2025-09-10\"\u003eReworking the blitter\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#xfade-2025-09-10\"\u003eBenchmarking masked crossfading\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#xfade-algs-2025-09-10\"\u003eAlgorithms\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#xfade-res-2025-09-10\"\u003eResults\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#xfade-future-2025-09-10\"\u003eFuture work\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#pi-2025-09-10\"\u003eOptimizing .PI image handling\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#pi-attempts-2025-09-10\"\u003eInitial attempts\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pi-piload-2025-09-10\"\u003eExtending PiLoad\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pi-th03-2025-09-10\"\u003eTH03's silly line-doubled sprite sheets\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#goal-2025-09-10\"\u003eMeeting our performance target (and uncovering ZUN bugs along with it)\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"wide-2025-09-10\"\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 The first part of this blitting performance series\u003c/a\u003e dealt with single-color transparent 8-wide and 16-wide sprites blitted to non-byte-aligned X positions, whose performance was the primary goal for the first release of the TH01 Anniversary Edition. Now that we're all about menus and cutscenes, we're dealing with the exact opposite kind of image: No transparency, always displayed at byte-aligned X positions, and much wider. The PC-98 doesn't offer any hardware support here, so this is purely about finding the fastest way to \u003ccode\u003ememcpy()\u003c/code\u003e from RAM to VRAM. We could just take Intel's CPU reference manuals, count cycles, pick the fastest method, and call it a day, but those manuals can't know about the PC-98's infamous hefty VRAM latencies or any potentially relevant differences between real hardware and emulators.\u003cbr\u003e\n\tThis time, I'll also provide the ASM code for each technique. All of the following code snippets show a single row of the dedicated blitter for 64-wide pixels, with the following register allocation:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ccode\u003eDS:SI\u003c/code\u003e points to the source sprite data in conventional RAM\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eES:DI\u003c/code\u003e points into VRAM\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eCX\u003c/code\u003e contains the stride of the source image\u003c/li\u003e\n\u003c/ul\u003e\u003col id=\"wide-algs-2025-09-10\"\u003e\u003cli\u003e\n\t\u003ci\u003eDisplaced\u003c/i\u003e\u003cbr\u003e\n\tLet's start with the method that is supposedly optimal on i486 CPUs, at least according to Intel's reference manual. \u003ca href=\"https://en.wikipedia.org/w/index.php?title=X86\u0026oldid=1307442739#Addressing_modes\"\u003ex86's addressing modes\u003c/a\u003e have always supported displacements for shifting the pointed-to address by a constant value, at the natural cost of a single code byte. This is the typical way you'd address structure fields from a pointer to an instance. In the context of blitting, this technique analogously treats the pixel chunks as fields within a \"VRAM row\" structure, and only updates the pointers at the end of each row to jump to the next one. Since all kinds of memory-accessing \u003ccode\u003eMOV\u003c/code\u003e instructions take just one cycle on an i486, this approach at least shouldn't add any CPU overhead on top of the unavoidable VRAM latencies.\n\t\u003cfigure\u003e\u003cpre\u003e; Row loop\nmov  \teax, [si]     \t; Pixels  0-31\nmov  \tes:[di], eax\nmov  \teax, [si+4]   \t; Pixels 32-63\nmov  \tes:[di+4], eax\n\n; Move to next row\nadd  \tsi, cx\nadd  \tdi, (640 / 8)\u003c/pre\u003e\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\u003ci\u003eMarching\u003c/i\u003e\u003cbr\u003e\n\tThese displacements sure are a nice feature of x86, but how big is their effect on performance really? Asked probably no one ever, considering that the alternative of constantly moving both pointers would add two more instructions that cost 6 bytes per 32-bit chunk, as well as \u0026gt;4 more CPU cycles on ≤386, not including instruction fetching costs as usual. I still had a column to fill in my benchmark output though, so why not fill it with a really bad approach that can demonstrate the impact of unoptimized code. Or lack thereof 👀\n\t\t\u003cfigure\u003e\u003cpre\u003e; We march by 64 pixels in every row.\nsub  \tcx, (64 / 8)\n\n; Row loop body\nmov  \teax, [si]   \t; Pixels  0-31\nmov  \tes:[di], eax\nadd  \tsi, 4\nadd  \tdi, 4\nmov  \teax, [si]   \t; Pixels 32-63\nmov  \tes:[di], eax\nadd  \tsi, 4\nadd  \tdi, 4\n\n; Move to next row\nadd  \tsi, cx\nadd  \tdi, ((640 - 64) / 8)\u003c/pre\u003e\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eMOVS\u003c/i\u003e\u003cbr\u003e\n\tMaybe, however, the marching approach isn't all that bad if we can express it more succinctly. In fact, the four-instruction \u003ccode\u003eMOV\u003c/code\u003e/\u003ccode\u003eMOV\u003c/code\u003e/\u003ccode\u003eADD\u003c/code\u003e/\u003ccode\u003eADD\u003c/code\u003e sequence does the same as the single \u003ccode\u003eMOVS\u003c/code\u003e instruction, and \u003ccode\u003eMOVS\u003c/code\u003e doesn't even require a temporary register! At \u003cspan class=\"hovertext\" title=\"Since MOVSD needs a size prefix byte in Real Mode, the complete instruction is as long as two MOVSW instructions. Therefore, they only differ in execution speed.\"\u003eone byte per 16 pixels\u003c/span\u003e, this would by far be the most compact method for shorter sprites. Our 2023 benchmark showed \u003ccode\u003eMOVS\u003c/code\u003e to be the preferred approach for 8- and 16-wide sprites on 286 and 386 CPUs, which matches Intel's documentation. Usually, \u003cq\u003ecomplex\u003c/q\u003e instructions like these are discouraged on later microarchitectures as they tend to get slower and slower, but maybe VRAM is even slower and it doesn't actually matter?\n\t\t\u003cfigure\u003e\u003cpre\u003e; We march by 64 pixels in every row.\nsub  \tcx, (64 / 8)\ncld  \t                    \t; Don't go backwards!\n\n; Row loop body\nmovsd\t                    \t; Pixels  0-31\nmovsd\t                    \t; Pixels 32-63\nadd  \tsi, cx              \t; Move to next row\nadd  \tdi, ((640 - 64) / 8)\u003c/pre\u003e\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eREP MOVS\u003c/i\u003e\u003cbr\u003e\n\tThanks to the \u003ccode\u003eREP\u003c/code\u003e prefix, we can compress such a series of \u003ccode\u003eMOVS\u003c/code\u003e instructions down even further and let the CPU handle the repetition. This is the preferred way you'd implement a \u003ccode\u003ememcpy()\u003c/code\u003e in hand-written x86 ASM if you're going for code clarity, but does it also perform well? Given the startup cost of \u003ccode\u003eREP\u003c/code\u003e, this will probably only ever be faster than \u003ccode\u003eMOVS\u003c/code\u003e past a certain sprite width. But it's almost certain to be the optimal approach for 640-wide sprites: Once the distance between the end of the previous row and the start of a new row becomes zero, we no longer need a vertical loop either, and can multiply the height into the repetition count to blit a whole bitplane in a single instruction.\n\t\t\u003cfigure\u003e\u003cpre\u003e; We might be blitting a smaller area out of a larger sprite.\nmov  \tax, cx              \t; The REP prefix needs CX itself\nsub  \tax, (64 / 8)\ncld  \t                    \t; Don't go backwards!\n\n; Row loop body\nmov  \tcx, (64 / 32)       \t; 64 pixels = 2 DWORDs\nrep movsd\n\n; Move to next row\nadd  \tsi, ax\nadd  \tdi, ((640 - 64) / 8)\u003c/pre\u003e\u003c/figure\u003e\n\t\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tSince we've thoroughly tested the GRCG against 4-plane blitting performance \u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 last time\u003c/a\u003e, we no longer need to check both. Instead, we can use the screen space for testing all possible image widths at 32-pixel intervals, using a self-made 640-wide dithered gradient:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-blitperf-wide.webp?2ecdece6\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"1507\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-10-blitperf-wide.avi?e29c1d82\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-blitperf-wide.webm?c92d7902\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-blitperf-wide.webm?6b80a6fe\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-blitperf-wide.webm?89f50f23\" type=\"video/webm\"\u003eVideo of the wide-sprite blitting benchmark running on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-blitperf-wide.avi?e29c1d82\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThis recording in Neko Project 21/W at 2.4576 × 27 = 66.3552 MHz might even be representative?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp id=\"wide-res-2025-09-10\"\u003e\n\tBut of course, the only relevant results are those from real hardware:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher class=\"blitperf benchmark-2025-09-10 inverted\"\u003e\n\t\u003ctable data-title=\"PC-9801DS/U2 (i386SX, 16 MHz, 1991)\" class=\"wide numbers active\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eDisplaced\u003c/th\u003e\n\t\u003cth\u003eMarching\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eMOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eREP\u0026nbsp;MOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e 1.47\u003c/td\u003e\u003ctd\u003e 1.56\u003c/td\u003e\u003ctd\u003e 1.34\u003c/td\u003e\u003ctd\u003e 1.35\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-wide-1991-PC9801DS-U2.svg?f704e6ed\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e 2.28\u003c/td\u003e\u003ctd\u003e 2.49\u003c/td\u003e\u003ctd\u003e 2.04\u003c/td\u003e\u003ctd\u003e 2.09\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e 3.16\u003c/td\u003e\u003ctd\u003e 3.51\u003c/td\u003e\u003ctd\u003e 2.84\u003c/td\u003e\u003ctd\u003e 2.84\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e 4.01\u003c/td\u003e\u003ctd\u003e 4.46\u003c/td\u003e\u003ctd\u003e 3.58\u003c/td\u003e\u003ctd\u003e 3.54\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e 4.92\u003c/td\u003e\u003ctd\u003e 5.52\u003c/td\u003e\u003ctd\u003e 4.39\u003c/td\u003e\u003ctd\u003e 4.34\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e 5.74\u003c/td\u003e\u003ctd\u003e 6.49\u003c/td\u003e\u003ctd\u003e 5.10\u003c/td\u003e\u003ctd\u003e 5.04\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e 6.72\u003c/td\u003e\u003ctd\u003e 7.58\u003c/td\u003e\u003ctd\u003e 5.78\u003c/td\u003e\u003ctd\u003e 5.80\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e 7.61\u003c/td\u003e\u003ctd\u003e 8.50\u003c/td\u003e\u003ctd\u003e 6.73\u003c/td\u003e\u003ctd\u003e 6.46\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e 8.35\u003c/td\u003e\u003ctd\u003e 9.55\u003c/td\u003e\u003ctd\u003e 7.44\u003c/td\u003e\u003ctd\u003e 7.18\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e 9.36\u003c/td\u003e\u003ctd\u003e10.51\u003c/td\u003e\u003ctd\u003e 8.18\u003c/td\u003e\u003ctd\u003e 7.89\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e10.26\u003c/td\u003e\u003ctd\u003e11.45\u003c/td\u003e\u003ctd\u003e 8.93\u003c/td\u003e\u003ctd\u003e 8.62\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e11.14\u003c/td\u003e\u003ctd\u003e12.48\u003c/td\u003e\u003ctd\u003e 9.68\u003c/td\u003e\u003ctd\u003e 9.30\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e12.03\u003c/td\u003e\u003ctd\u003e13.43\u003c/td\u003e\u003ctd\u003e10.17\u003c/td\u003e\u003ctd\u003e10.01\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e12.85\u003c/td\u003e\u003ctd\u003e14.34\u003c/td\u003e\u003ctd\u003e11.13\u003c/td\u003e\u003ctd\u003e10.72\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e13.71\u003c/td\u003e\u003ctd\u003e15.36\u003c/td\u003e\u003ctd\u003e11.83\u003c/td\u003e\u003ctd\u003e11.43\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e14.53\u003c/td\u003e\u003ctd\u003e16.23\u003c/td\u003e\u003ctd\u003e12.62\u003c/td\u003e\u003ctd\u003e12.13\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e15.51\u003c/td\u003e\u003ctd\u003e17.23\u003c/td\u003e\u003ctd\u003e13.31\u003c/td\u003e\u003ctd\u003e12.84\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e16.24\u003c/td\u003e\u003ctd\u003e18.25\u003c/td\u003e\u003ctd\u003e14.08\u003c/td\u003e\u003ctd\u003e13.54\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e17.10\u003c/td\u003e\u003ctd\u003e19.19\u003c/td\u003e\u003ctd\u003e14.57\u003c/td\u003e\u003ctd\u003e14.29\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e18.09\u003c/td\u003e\u003ctd\u003e20.04\u003c/td\u003e\u003ctd\u003e15.44\u003c/td\u003e\u003ctd\u003e14.67\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\n\t\u003ctable data-title=\"Neko Project 21/W (16 MHz)\" class=\"wide numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eDisplaced\u003c/th\u003e\n\t\u003cth\u003eMarching\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eMOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eREP\u0026nbsp;MOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.74\u003c/td\u003e\u003ctd\u003e0.76\u003c/td\u003e\u003ctd\u003e0.68\u003c/td\u003e\u003ctd\u003e0.67\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-wide-NP21-16-MHz.svg?99786281\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e1.06\u003c/td\u003e\u003ctd\u003e1.21\u003c/td\u003e\u003ctd\u003e0.98\u003c/td\u003e\u003ctd\u003e1.08\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e1.44\u003c/td\u003e\u003ctd\u003e1.64\u003c/td\u003e\u003ctd\u003e1.28\u003c/td\u003e\u003ctd\u003e1.41\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e1.78\u003c/td\u003e\u003ctd\u003e2.06\u003c/td\u003e\u003ctd\u003e1.58\u003c/td\u003e\u003ctd\u003e1.71\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e2.19\u003c/td\u003e\u003ctd\u003e2.51\u003c/td\u003e\u003ctd\u003e1.91\u003c/td\u003e\u003ctd\u003e2.03\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e2.52\u003c/td\u003e\u003ctd\u003e2.96\u003c/td\u003e\u003ctd\u003e2.22\u003c/td\u003e\u003ctd\u003e2.33\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e3.00\u003c/td\u003e\u003ctd\u003e3.47\u003c/td\u003e\u003ctd\u003e2.58\u003c/td\u003e\u003ctd\u003e2.74\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e3.31\u003c/td\u003e\u003ctd\u003e3.88\u003c/td\u003e\u003ctd\u003e2.88\u003c/td\u003e\u003ctd\u003e3.00\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e3.67\u003c/td\u003e\u003ctd\u003e4.29\u003c/td\u003e\u003ctd\u003e3.18\u003c/td\u003e\u003ctd\u003e3.31\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e4.00\u003c/td\u003e\u003ctd\u003e4.75\u003c/td\u003e\u003ctd\u003e3.50\u003c/td\u003e\u003ctd\u003e3.60\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e4.38\u003c/td\u003e\u003ctd\u003e5.14\u003c/td\u003e\u003ctd\u003e3.75\u003c/td\u003e\u003ctd\u003e3.89\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e4.74\u003c/td\u003e\u003ctd\u003e5.56\u003c/td\u003e\u003ctd\u003e4.05\u003c/td\u003e\u003ctd\u003e4.20\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e5.07\u003c/td\u003e\u003ctd\u003e6.00\u003c/td\u003e\u003ctd\u003e4.35\u003c/td\u003e\u003ctd\u003e4.49\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e5.43\u003c/td\u003e\u003ctd\u003e6.41\u003c/td\u003e\u003ctd\u003e4.64\u003c/td\u003e\u003ctd\u003e4.76\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e5.74\u003c/td\u003e\u003ctd\u003e6.83\u003c/td\u003e\u003ctd\u003e4.94\u003c/td\u003e\u003ctd\u003e5.07\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e6.13\u003c/td\u003e\u003ctd\u003e7.25\u003c/td\u003e\u003ctd\u003e5.25\u003c/td\u003e\u003ctd\u003e5.38\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e6.50\u003c/td\u003e\u003ctd\u003e7.69\u003c/td\u003e\u003ctd\u003e5.50\u003c/td\u003e\u003ctd\u003e5.66\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e6.82\u003c/td\u003e\u003ctd\u003e8.10\u003c/td\u003e\u003ctd\u003e5.81\u003c/td\u003e\u003ctd\u003e5.96\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e7.18\u003c/td\u003e\u003ctd\u003e8.50\u003c/td\u003e\u003ctd\u003e6.11\u003c/td\u003e\u003ctd\u003e6.25\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e7.50\u003c/td\u003e\u003ctd\u003e8.88\u003c/td\u003e\u003ctd\u003e6.32\u003c/td\u003e\u003ctd\u003e6.16\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\n\t\u003ctable data-title=\"Neko Project 21/W (66 MHz)\" class=\"wide numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eDisplaced\u003c/th\u003e\n\t\u003cth\u003eMarching\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eMOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003ccode\u003eREP\u0026nbsp;MOVS\u003c/code\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.16\u003c/td\u003e\u003ctd\u003e0.18\u003c/td\u003e\u003ctd\u003e0.15\u003c/td\u003e\u003ctd\u003e0.15\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-wide-NP21-66-MHz.svg?80512d02\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.25\u003c/td\u003e\u003ctd\u003e0.28\u003c/td\u003e\u003ctd\u003e0.22\u003c/td\u003e\u003ctd\u003e0.25\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.33\u003c/td\u003e\u003ctd\u003e0.38\u003c/td\u003e\u003ctd\u003e0.30\u003c/td\u003e\u003ctd\u003e0.33\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.42\u003c/td\u003e\u003ctd\u003e0.49\u003c/td\u003e\u003ctd\u003e0.37\u003c/td\u003e\u003ctd\u003e0.40\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.51\u003c/td\u003e\u003ctd\u003e0.59\u003c/td\u003e\u003ctd\u003e0.45\u003c/td\u003e\u003ctd\u003e0.48\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.60\u003c/td\u003e\u003ctd\u003e0.70\u003c/td\u003e\u003ctd\u003e0.52\u003c/td\u003e\u003ctd\u003e0.55\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.70\u003c/td\u003e\u003ctd\u003e0.82\u003c/td\u003e\u003ctd\u003e0.61\u003c/td\u003e\u003ctd\u003e0.64\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003ctd\u003e0.92\u003c/td\u003e\u003ctd\u003e0.68\u003c/td\u003e\u003ctd\u003e0.71\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e0.87\u003c/td\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e0.75\u003c/td\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e0.95\u003c/td\u003e\u003ctd\u003e1.13\u003c/td\u003e\u003ctd\u003e0.82\u003c/td\u003e\u003ctd\u003e0.85\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e1.04\u003c/td\u003e\u003ctd\u003e1.22\u003c/td\u003e\u003ctd\u003e0.89\u003c/td\u003e\u003ctd\u003e0.92\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e1.13\u003c/td\u003e\u003ctd\u003e1.33\u003c/td\u003e\u003ctd\u003e0.96\u003c/td\u003e\u003ctd\u003e1.00\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e1.20\u003c/td\u003e\u003ctd\u003e1.43\u003c/td\u003e\u003ctd\u003e1.03\u003c/td\u003e\u003ctd\u003e1.06\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e1.29\u003c/td\u003e\u003ctd\u003e1.53\u003c/td\u003e\u003ctd\u003e1.10\u003c/td\u003e\u003ctd\u003e1.13\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e1.38\u003c/td\u003e\u003ctd\u003e1.63\u003c/td\u003e\u003ctd\u003e1.17\u003c/td\u003e\u003ctd\u003e1.21\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e1.46\u003c/td\u003e\u003ctd\u003e1.73\u003c/td\u003e\u003ctd\u003e1.25\u003c/td\u003e\u003ctd\u003e1.28\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e1.54\u003c/td\u003e\u003ctd\u003e1.83\u003c/td\u003e\u003ctd\u003e1.31\u003c/td\u003e\u003ctd\u003e1.34\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e1.63\u003c/td\u003e\u003ctd\u003e1.94\u003c/td\u003e\u003ctd\u003e1.38\u003c/td\u003e\u003ctd\u003e1.42\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e1.71\u003c/td\u003e\u003ctd\u003e2.03\u003c/td\u003e\u003ctd\u003e1.46\u003c/td\u003e\u003ctd\u003e1.49\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e1.79\u003c/td\u003e\u003ctd\u003e2.13\u003c/td\u003e\u003ctd\u003e1.52\u003c/td\u003e\u003ctd\u003e1.47\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\tNumber of frames required to blit \u003cvar\u003eWidth\u003c/var\u003e×5120 pixels of a 1bpp bitmap to a byte-aligned position in VRAM, using the GRCG. The plots are relative to the respective \u003ccode\u003eREP MOVS\u003c/code\u003e result. Thanks to 1️⃣ オップナー2608 for the real-hardware test.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWait, only one run from a real system that's way below the minimum requirements of PC-98 Touhou? Furball is currently waiting for 286 and 486 hardware and will test those models once the shipment arrives. But all other test results I received came from Pentium-era or later models, and had \u003ci\u003eidentical\u003c/i\u003e performance for every blitting technique, \u003ci\u003eincluding\u003c/i\u003e the terrible marching one! Turns out that the superscalar microarchitecture of these CPUs erases any difference between blitting methods, and does so independently of clock speed. My testers and I replicated this behavior on\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ea PC-9821Na7 (clocked at 75 MHz),\u003c/li\u003e\n\t\u003cli\u003emy own PC-9821Nw133 (clocked at 133 MHz),\u003c/li\u003e\n\t\u003cli\u003ean AMD K6-III (clocked at 400 MHz), and\u003c/li\u003e\n\t\u003cli\u003ean AMD K6-2 (clocked at 533 MHz).\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThus, if your minimum target is a Pentium and you've got any kind of large image to blit to a byte-aligned position: Go with \u003ccode\u003eREP MOVS\u003c/code\u003e or even just \u003ccode\u003ememcpy()\u003c/code\u003e, enjoy having one generic blitter for every sprite width, and don't waste any further thoughts on the issue.\u003cbr\u003e\n\tBut since we have to make \u003ci\u003ea\u003c/i\u003e choice, we might as well optimize for the one target with stable performance characteristics that would be used by the most people: the i386 emulated by Neko Project, which prefers \u003ccode\u003eMOVS\u003c/code\u003e for all widths except 640 pixels.\n\u003c/p\u003e\u003chr id=\"blitter-2025-09-10\"\u003e\u003cp\u003e\n\tDeploying these blitting functions outside of a benchmark quickly runs into a practical problem. \u003ccode\u003eMOVS\u003c/code\u003e requires us to stay with the original plan of generating a dedicated blitter function for every possible byte-aligned sprite width. But this would mean that we'd unconditionally generate (\u003csup\u003e640\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e)\u0026nbsp;= 80 blitter functions and link them into every game. Most of these will not only never be used and just bloat the binaries, but also needlessly increase compile times due to how they are currently \u003ca href=\"https://github.com/nmlgc/ReC98/blob/9e3869341542d9fa8220056c972bf709a989bd54/platform/x86real/pc98/blit_low.hpp\"\u003estitched together out of an unholy mess of macros and force-inlined recursive functions\u003c/a\u003e. I'll probably fully drop down to ASM and one translation unit per blitter in the next iteration of this system; Turbo C++ 4.0J still generates several unnecessary instructions around \u003ca href=\"https://en.wikipedia.org/wiki/Duff%27s_device\"\u003eDuff's Device\u003c/a\u003e in particular. Now that I've constructed all these test cases, I really get how one could lose themselves in these tiny micro-optimizations. But I doubt that ZUN had the tools to measure the actual impact of what he did. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tFor now though, an opt-in model would be the next obvious step: Start with an empty blit function array, and let each game request and generate blitters for all the sizes it needs. But that just replaces the bloat with manual tedium: For deduplication reasons, this instantiation must happen in a dedicated (and obviously PC-98-exclusive) translation unit, far away from the actual blitting call in the (future cross-platform) translation unit that requires each size. Maybe this is practicable for a single game, but it gets really dumb if you have five games whose blitting widths are kind of similar except for the places where they aren't.\u003cbr\u003e\n\tAlso, the games will now crash if they ever try to blit at a width we didn't generate. Which in turn either requires a bloat-free way of reporting this error, or living with the annoyance of these mysterious crashes during development, because all the text areas in menus sure come in a wide variety of widths…\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, we'd like to have some kind of generic and width-independent blitter after all. Execution speed doesn't matter all too much for these menu use cases, which only call their blitters whenever any pixels are supposed to change. \u003ccode\u003eREP MOVSD\u003c/code\u003e would be the preferred generic method in such a blitter, and this choice is even well-informed now that the benchmarks have confirmed it to be at least the second-fastest option everywhere. Then, we only need two more conditional branches per line to cover the remaining pixels in case the width doesn't cleanly divide by 32:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e; BH: Sprite width in bytes\n; BL: 32-pixel chunks per row (BH / 4)\n; AX: Bytes between end of blitted width\n;     and start of next row (= stride - BH)\n\n\txor  \tcx, cx\n\n; Row loop body\n\tmov  \tcl, bl\n\trep movsd\n\n@@missing_16_or_24_pixels?:\n\ttest \tbh, 2\n\tjz   \t@@missing_8_pixels?\n\tmovsw\n\n@@missing_8_or_24_pixels?:\n\ttest \tbh, 1\n\tjz   \t@@next_row\n\tmovsb\n\n@@next_row:\n\tadd  \tsi, ax\n\tmov  \tcl, bh       \t; Roll back DI to\n\tsub  \tdi, cx       \t; start of row, and\n\tadd  \tdi, (640 / 8)\t; jump to the next.\u003c/pre\u003e\n\t\u003cfigcaption\u003eThe generic byte-aligned blitter, supporting any byte size between 8 and 2,048.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd since we still have this whole on-demand blitter generation framework for specific widths, games can still opt into faster blitters to accelerate commonly used sprite widths. TH01, for example, definitely gets back its fast 8- and 16-wide blitters for byte-aligned and pre-shifted pellets, that's for sure.\n\u003c/p\u003e\u003chr id=\"xfade-2025-09-10\"\u003e\u003cp\u003e\n\tIf that was the only kind of image blitting in menus and cutscenes, we'd be done with benchmarking now. But PC-98 Touhou also has one special effect we need to look at, which also happens to hold the most interesting unresolved performance question…\n\u003c/p\u003e\u003ch4\u003eMasked crossfading\u003c/h4\u003e\u003cp\u003e\n\tThis effect made its debut in TH03, where ZUN used it for a small number of the 320×200 picture transitions in the Stage 8/9 cutscenes and endings. In TH04 and TH05, it then became the default for most of these picture transitions. Its most prominent appearance, though, can be found at the end of TH05's title screen animation, where ZUN applies this effect to a full 640×400 image. Here's how this looks at 66\u0026nbsp;MHz on Neko Project 21/W, in all of its sluggish glory:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-TH05-Title-animation-original-66-MHz.webp?1df72cb9\" preload=\"none\" controls data-title=\"Original game\" loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"129\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-original-66-MHz.avi?f90a7dbe\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-TH05-Title-animation-original-66-MHz.webm?0b48ec90\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-TH05-Title-animation-original-66-MHz.webm?d21bdffa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-TH05-Title-animation-original-66-MHz.webm?bfd23615\" type=\"video/webm\"\u003eVideo of TH05's title screen crossfading effect, as rendered by the original game on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. Showcases how slow .PI blitting and crossfading adds a whole 60 frames of lag to the animation. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-original-66-MHz.avi?f90a7dbe\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"27\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"50\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"66\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"82\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"98\" data-title=\"Full picture\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAs you might have guessed, this effect works by only blitting certain pixels of the target image, as selected by a mask, on top of the current contents of VRAM. Each of these fade animations iterates over four fixed 4×4 masks that gradually show more pixels of the target image, before blitting it without a mask:\n\u003c/p\u003e\u003cfigure class=\"side_by_side small pixelated\" style=\"width: 256px;\"\u003e\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-10-PI-mask-0.webp?9ce4b60e\"\n\t\talt=\"PC-98 Touhou PI mask #1\"\n\t\twidth=\"256\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-10-PI-mask-1.webp?ccf6046a\"\n\t\talt=\"PC-98 Touhou PI mask #2\"\n\t\twidth=\"256\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-10-PI-mask-2.webp?46aa2645\"\n\t\talt=\"PC-98 Touhou PI mask #3\"\n\t\twidth=\"256\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-10-PI-mask-3.webp?6c48d13f\"\n\t\talt=\"PC-98 Touhou PI mask #4\"\n\t\twidth=\"256\"\n\t\tstyle=\"max-height: unset;\"\n\t\tclass=\"\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\tThe white/1 bits represent the new pixels of the destination image.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp id=\"xfade-algs-2025-09-10\"\u003e\n\tOn a system with packed 8-bit colors and a fast CPU, you could naively implement this effect by dropping down to drawing one pixel at a time and simply skipping over any pixel whose corresponding mask bit is not set. In a planar environment, however, you'll always have to perform a read-modify-write operation on a larger chunk of pixels. \u003ca href=\"https://en.wikipedia.org/w/index.php?title=Bit_blit\u0026oldid=1260205951#Technique\"\u003eWikipedia has a nice visualization of the general algorithm\u003c/a\u003e, which translates naturally to the planar VRAM of the PC-98:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eRead the source pixels from a VRAM bitplane and the target pixels from an image buffer in RAM\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eAND\u003c/code\u003e the VRAM bits with the \u003ci\u003enegated\u003c/i\u003e bits from the current mask row to erase the bits that we want to overwrite\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eAND\u003c/code\u003e the RAM bits with the bits from the current mask row to erase the bits we \u003ci\u003edon't\u003c/i\u003e want to overwrite\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eOR\u003c/code\u003e the two pixel chunks together and write the result back to VRAM\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOnce again, there are four ways in which we could implement this on a PC-98. The code examples assume 64-wide sprites once again:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eCPU only\u003c/i\u003e\u003cbr\u003e\n\tLet's start with the most naive approach that ignores all of the PC-98's blitter chips. Since we've decided on targeting Neko Project, we'd like to use x86 string instructions where possible, but we unfortunately can't do a direct copy due to the mask we have to apply to the source and destination pixels. But we \u003ci\u003ecan\u003c/i\u003e at least use \u003ccode\u003eLODSD\u003c/code\u003e for getting the source pixels into \u003ccode\u003eEAX\u003c/code\u003e while advancing the source pointer, and use displaced instructions to avoid marching the destination register.\u003cbr\u003e\n\tIf we go for the least amount of instructions, we end up with the following algorithm:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e; EAX: Source pixels\n; EBX: Source mask (1 = blit pixel)\n; ECX: Destination mask (1 = erase pixel)\n; [si_skip]: (image stride - (64 / 8))\n\n; Row loop body\nlodsd\t                \t; EAX = next 32 pixels at DS:[SI], then SI += 4\nand  \teax, ebx        \t; Clear out source bits we don't want to change\nand   \tes:[di], ecx    \t; Clear out destination bits we want to change\nor     \tes:[di], eax    \t; Pixels 0-31\nlodsd\t                \t; Repeat with a destination displacement for\nand  \teax, ebx        \t; pixels 32-63\nand   \tes:[di+4], ecx\nor     \tes:[di+4], eax\n\n; Move to the next row\nadd  \tsi, [bp-si_skip]\nadd  \tdi, (640 / 8)\u003c/pre\u003e\n\t\u003cfigcaption\u003eDX is already used for the image height, in case you're wondering about that variable on the stack.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHowever, this specific algorithm would turn out to be the worst thing we could possibly do. On paper, these memory-mutating \u003ccode\u003eAND\u003c/code\u003e and \u003ccode\u003eOR\u003c/code\u003e instructions might only cost 3 cycles on a 486, but both of them must transfer bits from VRAM to the CPU and back. That's two loads and two stores in a situation where we only really need one of each.\u003cbr\u003e\n\tWe could go for minimal VRAM accesses instead of blindly keeping instruction counts low, but then we'd end up with twice as many instructions:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e; EAX: Source pixels\n; EBX: Source mask (1 = blit pixel)\n; ECX: VRAM pixels\n; [si_skip]: (image stride - (64 / 8))\n\n; Row loop body\nlodsd\t                \t; EAX = next 32 pixels at DS:[SI], then SI += 4\nand  \teax, ebx        \t; Clear out source bits we don't want to change\nnot  \tebx             \t; Turn EBX into the destination mask\nmov  \tecx, es:[di]    \t; VRAM read\nand  \tecx, ebx        \t; Clear out destination bits we want to change\nor   \teax, ecx        \t; Combine source and VRAM\nstosd\t                \t; VRAM write; DI += 4\nnot  \tebx             \t; Turn EBX back into the source mask\nlodsd\t                \t; Repeat for pixels 32-63\nand  \teax, ebx\nnot  \tebx\nmov  \tecx, es:[di]\nand  \tecx, ebx\nor   \teax, ecx\nstosd\nnot  \tebx\n\n; Move to the next row\nadd  \tsi, [bp-si_skip]\nadd  \tdi, ((640 - 64) / 8)\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tBut sure enough, this version also runs almost twice as fast as my initial attempt above, across all systems we've tried!\u003cbr\u003e\n\tI still left a few micro-optimizations on the table with this one, though. Storing both masks on the stack might be faster than flipping \u003ccode\u003eEBX\u003c/code\u003e back and forth, and we should probably read from the per-row mask array through the \u003ccode\u003eFS\u003c/code\u003e or \u003ccode\u003eGS\u003c/code\u003e segment registers if we compile for ≥386 CPUs. But that shouldn't matter too much in view of all the more promising methods we've still got to try.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\n\t\u003ci\u003eGRCG + CPU\u003c/i\u003e\u003cbr\u003e\n\tThe simpler one of the PC-98's blitter chips is best used as part of a two-pass approach: First, we use the GRCG's RMW mode and a tile register filled with 0 bits to perform the erase step across the entire area of the image. The erase loop uses a simple \u003ccode\u003eREP STOS\u003c/code\u003e per image line and probably doesn't need any further optimizing attention if the \u003ccode\u003eREP MOVS\u003c/code\u003e result above is any indication. The second pass then only needs a mere three instructions per 32 pixels to perform the missing \u003ccode\u003eOR\u003c/code\u003e of the masked pixels:\n\u003cfigure\u003e\u003cpre\u003e; EAX: Source pixels\n; EBX: Source mask (1 = blit pixel)\n; CX: Free for mask retrieval!\n; [si_skip]: (image stride - (64 / 8))\n\n; Row loop body\nlodsd\t                \t; EAX = next 32 pixels at DS:[SI], then SI += 4\nand  \teax, ebx        \t; Clear out source bits we don't want to change\nor     \tes:[di], eax    \t; Pixels 0-31\nlodsd\t                \t; Repeat with a destination displacement for\nand  \teax, ebx        \t; pixels 32-63\nor     \tes:[di+4], eax\n\n; Move to the next row\nadd  \tsi, [bp-si_skip]\nadd  \tdi, (640 / 8)\u003c/pre\u003e\u003c/figure\u003e\n\tBut that looks eerily similar to the initial CPU attempt we scrapped earlier. The combination of a GRCG run in \u003cb\u003eR\u003c/b\u003eead-\u003cb\u003eM\u003c/b\u003eodify-\u003cb\u003eW\u003c/b\u003erite mode and the \u003ccode\u003eOR\u003c/code\u003e instruction means that we're back to two read-modify-write operations per pixel, one more than we actually need. Therefore, this approach can only outperform our fast CPU algorithm if the GRCG run manages to be faster than the 5×4 extra instructions we need to perform the erase step on the CPU.\u003cbr\u003e\n\tAnd that's indeed the best use we can get out of the GRCG for this effect. The GRCG is hardwired for blitting a rather static 8-pixel tile with a more variable mask provided by the CPU, but this effect calls for the exact opposite, a static mask and variable pixels. If we wanted to draw the \u003ci\u003eentire\u003c/i\u003e effect using the GRCG, we'd have to send our source pixels through slow I/O ports 8 pixels at a time, which already wastes all the added throughput we could get from a 32-bit and even 16-bit CPU. Meanwhile, the mask register on the CPU – \u003ci\u003ethe thing we can easily change\u003c/i\u003e – only changes on every \u003ci\u003eline\u003c/i\u003e of pixels.\u003cbr\u003e\n\tThe initial blit of the Siddhaṃ seed syllables at the beginning of TH01's Konngara fight roughly demonstrates how slow such an approach would be. BERO's PiLoad library implements transparent blitting in a similar way, sending each decoded byte to the GRCG and then blitting it with a mask:\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.webp?f89a792b\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"49\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.avi?3ddac65a\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.webm?ea238947\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.webm?5f93f076\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.webm?c5d62120\" type=\"video/webm\"\u003eVideo of the blitting process of the Siddhaṃ seed syllables at the start of TH01's Konngara fight, which are drawn using the transparency feature of BERO's PiLoad library. Recorded on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-TH01-Konngara-Siddhaṃ-blit-66-MHz.avi?3ddac65a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eBOSS8_D1.GRP\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"12\" data-title=\"\u003ccode\u003eBOSS8_D2.GRP\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"24\" data-title=\"\u003ccode\u003eBOSS8_D3.GRP\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"37\" data-title=\"\u003ccode\u003eBOSS8_D4.GRP\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eAll of these images would decompress within 6 frames on Neko Project 21/W at 66\u0026nbsp;MHz, but PiLoad's transparent blitting algorithm easily doubles that. That's what, ~36,750 cycles for every single line of the image?\u003cbr\u003e\n\t\tThe resulting rolling effect is another great example of CPU-speed-dependent lag. Everything here is supposed to happen within a single logical frame, and only doesn't because the system is too slow. A faster PC-98 would blit the image more quickly and thus spend fewer frames, and ports will display all four syllables instantly.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cp\u003e\n\tFortunately, the PC-98 also has a second blitter chip that is much more suited to what we're trying to do here, …\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eEGC (ZUN's Version)\u003c/i\u003e\u003cbr\u003e\n\t…and it also happens to be the one ZUN used in his original code. But isn't the EGC infamous for only supporting block copies if the source pixels are already in VRAM? How would we get them there in a setting where we show a full-screen 640×400 image and don't hide \u003ci\u003eanything\u003c/i\u003e using \u003ca href=\"/blog/2023-03-30#tiles-2023-03-30\"\u003e📝 black cells on the text layer\u003c/a\u003e?\u003cbr\u003e\n\tThe answer can be found by looking at \u003ca href=\"https://radioc.web.fc2.com/column/pc98bas/pc98memmap_en.htm\"\u003ethe PC-98's memory map\u003c/a\u003e. Each bitplane requires the physical existence of (\u003csup\u003e640\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e\u0026nbsp;×\u0026nbsp;400)\u0026nbsp;= 32,000 bytes of memory. But that's a rather odd number for a memory map, which typically prefers sizes that are powers of 2. And so, NEC not only rounded up the area in the memory map, but also the size of physically available VRAM, giving us 32,\u003cstrong\u003e768\u003c/strong\u003e bytes per bitplane and page. That's an extra \u003cspan class=\"hovertext\" title=\"×2 if you consider that we have two pages, but we're not going to ruin our performance with a page flip every 16 pixels, as I've thoroughly explained in earlier posts.\"\u003e768×4 bytes\u003c/span\u003e of memory that isn't shown on screen but still counts as VRAM and can be accessed by the EGC.\u003cbr\u003e\n\tThis allowed ZUN to go for a line-by-line approach using the EGC's mask register (\u003ccode\u003e0x4A8\u003c/code\u003e):\u003c/p\u003e\u003col\u003e\n\t\t\u003cli\u003eRegularly blit the source pixels of the current line to the beginning of each bitplane's offscreen section\u003c/li\u003e\n\t\t\u003cli\u003eTurn on the EGC, configure it for a regular VRAM-to-VRAM copy, and set the mask register depending on the current row\u003c/li\u003e\n\t\t\u003cli\u003eEGC-blit the line to its intended position\u003c/li\u003e\n\t\t\u003cli\u003eTurn off the EGC\u003c/li\u003e\n\t\t\u003cli\u003eRepeat for every line of the image\u003c/li\u003e\n\t\u003c/ol\u003e\u003cp\u003e\n\tFrom all we've learned about PC-98 hardware so far, this should still be pretty slow. While we're back to a single read-modify-write operation for the masked blit itself, the initial blit of the source pixels to the offscreen area still adds one extra VRAM write per 32 pixels, on an architecture that's already notorious for having slow VRAM. Worse, however, is the fact that configuring the EGC still involves these slow I/O port writes I've been writing about time and time again. EGC-accelerated blitting would have to be \u003ci\u003ereally\u003c/i\u003e fast to be worth the added I/O cost of this approach. \u003ca href=\"/blog/2021-02-21\"\u003e📝 I sure couldn't believe that this was actually faster when I first saw this piece of ZUN code in early 2021\u003c/a\u003e, but he probably was onto something here.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\u003ci\u003eEGC (optimized)\u003c/i\u003e\u003cbr\u003e\n\tBut wait. Blitting only one line in each EGC run only utilizes (\u003csup\u003e640\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e)\u0026nbsp;=80 of the 768 available offscreen bytes we get on each bitplane. How much faster could we go if we used the \u003ci\u003eentire\u003c/i\u003e offscreen area during each run?\u003cbr\u003e\n\tAfter all, nothing stops us from treating invisible VRAM as regular linear memory for 4bpp pixel data, ignoring the spatial layout of the source image. For TH05's 640×400 title screen image, we can fit ⌊\u003csup\u003e768\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e(\u003csup\u003e640\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e)\u003c/sub\u003e⌋\u0026nbsp;= 9 lines of pixels into the offscreen area, which reduces the number of EGC runs from 400 to just 45. For the crossfaded 320×200 cutscene pictures, we get ⌊\u003csup\u003e768\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e(\u003csup\u003e320\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e\u003c/sub\u003e)⌋\u0026nbsp;= 19 lines and \u003ci\u003e11\u003c/i\u003e EGC runs, wasting even less VRAM.\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-xfade-EGC-optimized.webp?ce29b359\" preload=\"none\" controls loop width=\"640\" height=\"410\" data-fps=\"5\" data-frame-count=\"14\" style=\"aspect-ratio: 640 / 410\" data-lossless=\"/blog/static/video/zmbv/2025-09-10-xfade-EGC-optimized.avi?b4f03d12\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-xfade-EGC-optimized.webm?645b5ad5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-xfade-EGC-optimized.webm?294327d1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-xfade-EGC-optimized.webm?34c8d6c6\" type=\"video/webm\"\u003eVisualization of optimized EGC-powered crossfading, demonstrated with TH03 Yumemi's \"battle wear\" image. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-xfade-EGC-optimized.avi?b4f03d12\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eA visualization of the 11 EGC runs required for masked blitting of a 320×200 cutscene picture. The offscreen area of VRAM is highlighted in \u003cspan style=\"color:#f0f\"\u003epink\u003c/span\u003e on the first frame; the white stripe on its last line represents the nonexistent memory past 32,768 bytes, since 768 bytes do not cleanly divide into 640-pixel rows.\u003cbr\u003e\n\t\tFor 320-wide images, this approach only leaves 64 pixels (or 8 bytes) of VRAM unused. Micro-optimizing those last few bytes won't matter – not only because we'd still need 11 EGC runs even if we blitted 19.2 lines on each frame instead of 19, but also because the mask patterns force us to iterate over discrete rows anyway.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\tAside from this major algorithmic optimization, this approach also offers a vast potential for micro-optimizations:\n\t\u003cul\u003e\n\t\t\u003cli\u003eThe EGC retains its registers across activations, so we only need to set them up once at the beginning of the image. This saves an additional 5 I/O port writes per EGC run over ZUN's version.\u003c/li\u003e\n\t\t\u003cli\u003eUsing \u003ccode\u003eREP MOVSW\u003c/code\u003e for the EGC copy not only follows from our previous results, but also makes sense because we can use a segment override prefix to overwrite \u003ccode\u003eMOVSW\u003c/code\u003e's source register and thus make it copy entirely within the \u003ccode\u003eES\u003c/code\u003e register. Since we still access the pixel masks for each line through \u003ccode\u003eDS\u003c/code\u003e, using an \u003ccode\u003eES\u003c/code\u003e override removes the need for the awkward \u003ccode\u003eDS\u003c/code\u003e register juggling that ZUN had to do for each line of the image.\u003c/li\u003e\n\t\t\u003cli\u003eOur linear nature of accessing offscreen VRAM necessitates a new set of blitters that doesn't move \u003ccode\u003eDI\u003c/code\u003e to the next VRAM row at the end of an image row. \u003ci\u003eHowever\u003c/i\u003e, we only actually need these blitters for their ability to skip to the next line of the \u003ci\u003esource\u003c/i\u003e image in case we only blit a subregion whose width is smaller than the width of the image. If we blit an image's entire width, we can go down an even faster code path that blasts all lines of an EGC run to VRAM with a single \u003ccode\u003eREP MOVS\u003c/code\u003e per bitplane. As we've seen for the 640-pixel case above, this is always the optimal choice.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp id=\"xfade-res-2025-09-10\"\u003e\n\tSo, let's construct another benchmark, do some preliminary tests on Neko Project 21/W, and…\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-blitperf-xfade.webp?d99c121f\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"722\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-09-10-blitperf-xfade.avi?4b84cbbf\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-blitperf-xfade.webm?f526dde6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-blitperf-xfade.webm?1783ec9d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-blitperf-xfade.webm?7001249e\" type=\"video/webm\"\u003eVideo of the masked crossfading benchmark running on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-blitperf-xfade.avi?4b84cbbf\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t…whoa, ZUN's EGC algorithm is \u003ci\u003ebad\u003c/i\u003e on emulators. This benchmark even paints it somewhat favorably; for code simplicity reasons, I merely simulate it within my optimized implementation by reducing the number of blitted image rows per EGC run to 1.\u003cbr\u003e\n\tLet's see how fast it all runs on real hardware. This time, the results for ≥Pentium models are interesting as well:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher class=\"blitperf benchmark-2025-09-10 inverted\"\u003e\n\t\u003ctable data-title=\"PC-9801DS/U2 (i386SX, 16 MHz, 1991)\" class=\"xfade numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003ctd\u003e2.50\u003c/td\u003e\u003ctd\u003e1.15\u003c/td\u003e\u003ctd\u003e1.11\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-1991-PC9801DS-U2.svg?b5097fbd\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e2.90\u003c/td\u003e\u003ctd\u003e1.65\u003c/td\u003e\u003ctd\u003e1.64\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e1.36\u003c/td\u003e\u003ctd\u003e3.26\u003c/td\u003e\u003ctd\u003e2.14\u003c/td\u003e\u003ctd\u003e2.14\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e1.72\u003c/td\u003e\u003ctd\u003e3.56\u003c/td\u003e\u003ctd\u003e2.60\u003c/td\u003e\u003ctd\u003e2.64\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e2.07\u003c/td\u003e\u003ctd\u003e3.79\u003c/td\u003e\u003ctd\u003e3.07\u003c/td\u003e\u003ctd\u003e3.14\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e2.42\u003c/td\u003e\u003ctd\u003e4.15\u003c/td\u003e\u003ctd\u003e3.58\u003c/td\u003e\u003ctd\u003e3.65\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e2.81\u003c/td\u003e\u003ctd\u003e4.34\u003c/td\u003e\u003ctd\u003e4.06\u003c/td\u003e\u003ctd\u003e4.15\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e3.15\u003c/td\u003e\u003ctd\u003e4.68\u003c/td\u003e\u003ctd\u003e4.52\u003c/td\u003e\u003ctd\u003e4.65\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e3.49\u003c/td\u003e\u003ctd\u003e5.02\u003c/td\u003e\u003ctd\u003e5.01\u003c/td\u003e\u003ctd\u003e5.15\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e3.82\u003c/td\u003e\u003ctd\u003e5.35\u003c/td\u003e\u003ctd\u003e5.52\u003c/td\u003e\u003ctd\u003e5.66\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e4.20\u003c/td\u003e\u003ctd\u003e5.70\u003c/td\u003e\u003ctd\u003e5.99\u003c/td\u003e\u003ctd\u003e6.17\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e4.52\u003c/td\u003e\u003ctd\u003e6.03\u003c/td\u003e\u003ctd\u003e6.44\u003c/td\u003e\u003ctd\u003e6.66\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e4.88\u003c/td\u003e\u003ctd\u003e6.36\u003c/td\u003e\u003ctd\u003e6.92\u003c/td\u003e\u003ctd\u003e7.16\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e5.20\u003c/td\u003e\u003ctd\u003e6.71\u003c/td\u003e\u003ctd\u003e7.43\u003c/td\u003e\u003ctd\u003e7.68\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e5.58\u003c/td\u003e\u003ctd\u003e7.04\u003c/td\u003e\u003ctd\u003e7.93\u003c/td\u003e\u003ctd\u003e8.17\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e5.91\u003c/td\u003e\u003ctd\u003e7.38\u003c/td\u003e\u003ctd\u003e8.36\u003c/td\u003e\u003ctd\u003e8.67\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e6.26\u003c/td\u003e\u003ctd\u003e7.72\u003c/td\u003e\u003ctd\u003e8.85\u003c/td\u003e\u003ctd\u003e9.18\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e6.59\u003c/td\u003e\u003ctd\u003e8.05\u003c/td\u003e\u003ctd\u003e9.39\u003c/td\u003e\u003ctd\u003e9.68\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e6.95\u003c/td\u003e\u003ctd\u003e8.40\u003c/td\u003e\u003ctd\u003e9.84\u003c/td\u003e\u003ctd\u003e10.18\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e7.30\u003c/td\u003e\u003ctd\u003e8.65\u003c/td\u003e\u003ctd\u003e10.39\u003c/td\u003e\u003ctd\u003e10.66\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003ctable data-title=\"PC-9821Na7 (Pentium, 75 MHz, 1995)\" class=\"xfade numbers active\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.17\u003c/td\u003e\u003ctd\u003e0.41\u003c/td\u003e\u003ctd\u003e0.23\u003c/td\u003e\u003ctd\u003e0.23\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-1995-PC9821Na7.svg?96b21b08\"\u003e\n\t\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.27\u003c/td\u003e\u003ctd\u003e0.51\u003c/td\u003e\u003ctd\u003e0.40\u003c/td\u003e\u003ctd\u003e0.39\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.38\u003c/td\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003ctd\u003e0.58\u003c/td\u003e\u003ctd\u003e0.56\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.48\u003c/td\u003e\u003ctd\u003e0.72\u003c/td\u003e\u003ctd\u003e0.76\u003c/td\u003e\u003ctd\u003e0.73\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.60\u003c/td\u003e\u003ctd\u003e0.79\u003c/td\u003e\u003ctd\u003e0.95\u003c/td\u003e\u003ctd\u003e0.91\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.70\u003c/td\u003e\u003ctd\u003e0.89\u003c/td\u003e\u003ctd\u003e1.13\u003c/td\u003e\u003ctd\u003e1.08\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.80\u003c/td\u003e\u003ctd\u003e0.98\u003c/td\u003e\u003ctd\u003e1.30\u003c/td\u003e\u003ctd\u003e1.25\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.90\u003c/td\u003e\u003ctd\u003e1.09\u003c/td\u003e\u003ctd\u003e1.47\u003c/td\u003e\u003ctd\u003e1.42\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e1.21\u003c/td\u003e\u003ctd\u003e1.67\u003c/td\u003e\u003ctd\u003e1.60\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e1.13\u003c/td\u003e\u003ctd\u003e1.31\u003c/td\u003e\u003ctd\u003e1.84\u003c/td\u003e\u003ctd\u003e1.77\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e1.23\u003c/td\u003e\u003ctd\u003e1.41\u003c/td\u003e\u003ctd\u003e2.02\u003c/td\u003e\u003ctd\u003e1.93\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e1.33\u003c/td\u003e\u003ctd\u003e1.51\u003c/td\u003e\u003ctd\u003e2.19\u003c/td\u003e\u003ctd\u003e2.10\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e1.45\u003c/td\u003e\u003ctd\u003e1.63\u003c/td\u003e\u003ctd\u003e2.39\u003c/td\u003e\u003ctd\u003e2.29\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e1.56\u003c/td\u003e\u003ctd\u003e1.74\u003c/td\u003e\u003ctd\u003e2.57\u003c/td\u003e\u003ctd\u003e2.45\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e1.66\u003c/td\u003e\u003ctd\u003e1.83\u003c/td\u003e\u003ctd\u003e2.74\u003c/td\u003e\u003ctd\u003e2.62\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e1.76\u003c/td\u003e\u003ctd\u003e1.93\u003c/td\u003e\u003ctd\u003e2.91\u003c/td\u003e\u003ctd\u003e2.78\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e1.86\u003c/td\u003e\u003ctd\u003e2.04\u003c/td\u003e\u003ctd\u003e3.09\u003c/td\u003e\u003ctd\u003e2.95\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e1.97\u003c/td\u003e\u003ctd\u003e2.14\u003c/td\u003e\u003ctd\u003e3.27\u003c/td\u003e\u003ctd\u003e3.12\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e2.07\u003c/td\u003e\u003ctd\u003e2.24\u003c/td\u003e\u003ctd\u003e3.44\u003c/td\u003e\u003ctd\u003e3.28\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e2.17\u003c/td\u003e\u003ctd\u003e2.34\u003c/td\u003e\u003ctd\u003e3.61\u003c/td\u003e\u003ctd\u003e3.45\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003ctable data-title=\"PC-9821Nw133 (Pentium, 133 MHz, 1997)\" class=\"xfade numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.09\u003c/td\u003e\u003ctd\u003e0.21\u003c/td\u003e\u003ctd\u003e0.15\u003c/td\u003e\u003ctd\u003e0.14\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-1997-PC9821Nw133.svg?a9da8b61\"\u003e\n\t\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.17\u003c/td\u003e\u003ctd\u003e0.28\u003c/td\u003e\u003ctd\u003e0.30\u003c/td\u003e\u003ctd\u003e0.27\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.25\u003c/td\u003e\u003ctd\u003e0.36\u003c/td\u003e\u003ctd\u003e0.44\u003c/td\u003e\u003ctd\u003e0.41\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.33\u003c/td\u003e\u003ctd\u003e0.43\u003c/td\u003e\u003ctd\u003e0.59\u003c/td\u003e\u003ctd\u003e0.55\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.41\u003c/td\u003e\u003ctd\u003e0.52\u003c/td\u003e\u003ctd\u003e0.74\u003c/td\u003e\u003ctd\u003e0.68\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.49\u003c/td\u003e\u003ctd\u003e0.59\u003c/td\u003e\u003ctd\u003e0.89\u003c/td\u003e\u003ctd\u003e0.82\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.58\u003c/td\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003ctd\u003e1.03\u003c/td\u003e\u003ctd\u003e0.95\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003ctd\u003e0.75\u003c/td\u003e\u003ctd\u003e1.18\u003c/td\u003e\u003ctd\u003e1.09\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e0.74\u003c/td\u003e\u003ctd\u003e0.84\u003c/td\u003e\u003ctd\u003e1.33\u003c/td\u003e\u003ctd\u003e1.22\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e0.82\u003c/td\u003e\u003ctd\u003e0.91\u003c/td\u003e\u003ctd\u003e1.48\u003c/td\u003e\u003ctd\u003e1.36\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e0.90\u003c/td\u003e\u003ctd\u003e0.99\u003c/td\u003e\u003ctd\u003e1.62\u003c/td\u003e\u003ctd\u003e1.49\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e0.99\u003c/td\u003e\u003ctd\u003e1.08\u003c/td\u003e\u003ctd\u003e1.77\u003c/td\u003e\u003ctd\u003e1.63\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e1.07\u003c/td\u003e\u003ctd\u003e1.17\u003c/td\u003e\u003ctd\u003e1.92\u003c/td\u003e\u003ctd\u003e1.76\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e1.15\u003c/td\u003e\u003ctd\u003e1.24\u003c/td\u003e\u003ctd\u003e2.06\u003c/td\u003e\u003ctd\u003e1.90\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e1.23\u003c/td\u003e\u003ctd\u003e1.32\u003c/td\u003e\u003ctd\u003e2.21\u003c/td\u003e\u003ctd\u003e2.03\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e1.31\u003c/td\u003e\u003ctd\u003e1.40\u003c/td\u003e\u003ctd\u003e2.35\u003c/td\u003e\u003ctd\u003e2.17\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e1.40\u003c/td\u003e\u003ctd\u003e1.48\u003c/td\u003e\u003ctd\u003e2.50\u003c/td\u003e\u003ctd\u003e2.30\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e1.48\u003c/td\u003e\u003ctd\u003e1.56\u003c/td\u003e\u003ctd\u003e2.65\u003c/td\u003e\u003ctd\u003e2.44\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e1.56\u003c/td\u003e\u003ctd\u003e1.65\u003c/td\u003e\u003ctd\u003e2.80\u003c/td\u003e\u003ctd\u003e2.57\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e1.64\u003c/td\u003e\u003ctd\u003e1.73\u003c/td\u003e\u003ctd\u003e2.94\u003c/td\u003e\u003ctd\u003e2.71\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003ctable data-title=\"AMD K6-III (400 MHz)\" class=\"xfade numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.08\u003c/td\u003e\u003ctd\u003e0.16\u003c/td\u003e\u003ctd\u003e0.14\u003c/td\u003e\u003ctd\u003e0.13\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-K6-III.svg?d9f22bca\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.16\u003c/td\u003e\u003ctd\u003e0.23\u003c/td\u003e\u003ctd\u003e0.29\u003c/td\u003e\u003ctd\u003e0.26\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.24\u003c/td\u003e\u003ctd\u003e0.31\u003c/td\u003e\u003ctd\u003e0.43\u003c/td\u003e\u003ctd\u003e0.39\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.32\u003c/td\u003e\u003ctd\u003e0.39\u003c/td\u003e\u003ctd\u003e0.57\u003c/td\u003e\u003ctd\u003e0.52\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.40\u003c/td\u003e\u003ctd\u003e0.46\u003c/td\u003e\u003ctd\u003e0.72\u003c/td\u003e\u003ctd\u003e0.65\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.48\u003c/td\u003e\u003ctd\u003e0.54\u003c/td\u003e\u003ctd\u003e0.86\u003c/td\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.56\u003c/td\u003e\u003ctd\u003e0.62\u003c/td\u003e\u003ctd\u003e1.00\u003c/td\u003e\u003ctd\u003e0.92\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.64\u003c/td\u003e\u003ctd\u003e0.70\u003c/td\u003e\u003ctd\u003e1.15\u003c/td\u003e\u003ctd\u003e1.05\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e0.71\u003c/td\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003ctd\u003e1.29\u003c/td\u003e\u003ctd\u003e1.18\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e0.79\u003c/td\u003e\u003ctd\u003e0.86\u003c/td\u003e\u003ctd\u003e1.43\u003c/td\u003e\u003ctd\u003e1.31\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e0.87\u003c/td\u003e\u003ctd\u003e0.93\u003c/td\u003e\u003ctd\u003e1.58\u003c/td\u003e\u003ctd\u003e1.44\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e0.95\u003c/td\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e1.72\u003c/td\u003e\u003ctd\u003e1.57\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e1.03\u003c/td\u003e\u003ctd\u003e1.09\u003c/td\u003e\u003ctd\u003e1.86\u003c/td\u003e\u003ctd\u003e1.70\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e1.11\u003c/td\u003e\u003ctd\u003e1.17\u003c/td\u003e\u003ctd\u003e2.01\u003c/td\u003e\u003ctd\u003e1.83\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e1.19\u003c/td\u003e\u003ctd\u003e1.25\u003c/td\u003e\u003ctd\u003e2.15\u003c/td\u003e\u003ctd\u003e1.96\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e1.27\u003c/td\u003e\u003ctd\u003e1.33\u003c/td\u003e\u003ctd\u003e2.29\u003c/td\u003e\u003ctd\u003e2.09\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e1.35\u003c/td\u003e\u003ctd\u003e1.41\u003c/td\u003e\u003ctd\u003e2.44\u003c/td\u003e\u003ctd\u003e2.22\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e1.43\u003c/td\u003e\u003ctd\u003e1.49\u003c/td\u003e\u003ctd\u003e2.58\u003c/td\u003e\u003ctd\u003e2.35\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e1.50\u003c/td\u003e\u003ctd\u003e1.56\u003c/td\u003e\u003ctd\u003e2.72\u003c/td\u003e\u003ctd\u003e2.48\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e1.58\u003c/td\u003e\u003ctd\u003e1.64\u003c/td\u003e\u003ctd\u003e2.87\u003c/td\u003e\u003ctd\u003e2.61\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003ctable data-title=\"PC-9821V166 Shooting Star (upgraded to K6-2, 533 MHz, 1997)\" class=\"xfade numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.10\u003c/td\u003e\u003ctd\u003e0.24\u003c/td\u003e\u003ctd\u003e0.17\u003c/td\u003e\u003ctd\u003e0.16\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-1997-PC9821V166-K6-2.svg?775f6b83\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.19\u003c/td\u003e\u003ctd\u003e0.33\u003c/td\u003e\u003ctd\u003e0.34\u003c/td\u003e\u003ctd\u003e0.31\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.29\u003c/td\u003e\u003ctd\u003e0.40\u003c/td\u003e\u003ctd\u003e0.50\u003c/td\u003e\u003ctd\u003e0.46\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.38\u003c/td\u003e\u003ctd\u003e0.48\u003c/td\u003e\u003ctd\u003e0.67\u003c/td\u003e\u003ctd\u003e0.61\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.47\u003c/td\u003e\u003ctd\u003e0.57\u003c/td\u003e\u003ctd\u003e0.83\u003c/td\u003e\u003ctd\u003e0.77\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.56\u003c/td\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003ctd\u003e1.00\u003c/td\u003e\u003ctd\u003e0.92\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.65\u003c/td\u003e\u003ctd\u003e0.75\u003c/td\u003e\u003ctd\u003e1.16\u003c/td\u003e\u003ctd\u003e1.07\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.75\u003c/td\u003e\u003ctd\u003e0.84\u003c/td\u003e\u003ctd\u003e1.33\u003c/td\u003e\u003ctd\u003e1.22\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e0.84\u003c/td\u003e\u003ctd\u003e0.93\u003c/td\u003e\u003ctd\u003e1.50\u003c/td\u003e\u003ctd\u003e1.38\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e0.93\u003c/td\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e1.66\u003c/td\u003e\u003ctd\u003e1.53\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003ctd\u003e1.11\u003c/td\u003e\u003ctd\u003e1.83\u003c/td\u003e\u003ctd\u003e1.69\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e1.11\u003c/td\u003e\u003ctd\u003e1.21\u003c/td\u003e\u003ctd\u003e1.99\u003c/td\u003e\u003ctd\u003e1.83\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e1.21\u003c/td\u003e\u003ctd\u003e1.30\u003c/td\u003e\u003ctd\u003e2.16\u003c/td\u003e\u003ctd\u003e1.99\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e1.30\u003c/td\u003e\u003ctd\u003e1.39\u003c/td\u003e\u003ctd\u003e2.33\u003c/td\u003e\u003ctd\u003e2.14\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e1.39\u003c/td\u003e\u003ctd\u003e1.48\u003c/td\u003e\u003ctd\u003e2.49\u003c/td\u003e\u003ctd\u003e2.29\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e1.48\u003c/td\u003e\u003ctd\u003e1.57\u003c/td\u003e\u003ctd\u003e2.66\u003c/td\u003e\u003ctd\u003e2.45\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e1.57\u003c/td\u003e\u003ctd\u003e1.66\u003c/td\u003e\u003ctd\u003e2.82\u003c/td\u003e\u003ctd\u003e2.60\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e1.67\u003c/td\u003e\u003ctd\u003e1.76\u003c/td\u003e\u003ctd\u003e2.99\u003c/td\u003e\u003ctd\u003e2.75\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e1.76\u003c/td\u003e\u003ctd\u003e1.84\u003c/td\u003e\u003ctd\u003e3.15\u003c/td\u003e\u003ctd\u003e2.90\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e1.85\u003c/td\u003e\u003ctd\u003e1.94\u003c/td\u003e\u003ctd\u003e3.32\u003c/td\u003e\u003ctd\u003e3.06\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003ctable data-title=\"Neko Project 21/W (66 MHz)\" class=\"xfade numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eWidth\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(optimized)\u003c/th\u003e\n\t\u003cth\u003eEGC\u003cbr\u003e(ZUN)\u003c/th\u003e\n\t\u003cth\u003eGRCG\u0026nbsp;+\u003cbr\u003eCPU\u003c/th\u003e\n\t\u003cth\u003eCPU\u003cbr\u003eonly\u003c/th\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\t\t\t\u003cth\u003e 32\u003c/th\u003e\u003ctd\u003e0.08\u003c/td\u003e\u003ctd\u003e0.44\u003c/td\u003e\u003ctd\u003e0.20\u003c/td\u003e\u003ctd\u003e0.18\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"20\"\u003e\n\t\t\t\t\u003cembed src=\"/blog/static/2025-09-10-blitperf-xfade-NP21-66-MHz.svg?f293c543\"\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 64\u003c/th\u003e\u003ctd\u003e0.11\u003c/td\u003e\u003ctd\u003e0.47\u003c/td\u003e\u003ctd\u003e0.25\u003c/td\u003e\u003ctd\u003e0.24\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e 96\u003c/th\u003e\u003ctd\u003e0.15\u003c/td\u003e\u003ctd\u003e0.51\u003c/td\u003e\u003ctd\u003e0.30\u003c/td\u003e\u003ctd\u003e0.30\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e128\u003c/th\u003e\u003ctd\u003e0.18\u003c/td\u003e\u003ctd\u003e0.54\u003c/td\u003e\u003ctd\u003e0.36\u003c/td\u003e\u003ctd\u003e0.36\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e160\u003c/th\u003e\u003ctd\u003e0.22\u003c/td\u003e\u003ctd\u003e0.56\u003c/td\u003e\u003ctd\u003e0.41\u003c/td\u003e\u003ctd\u003e0.42\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e192\u003c/th\u003e\u003ctd\u003e0.25\u003c/td\u003e\u003ctd\u003e0.59\u003c/td\u003e\u003ctd\u003e0.46\u003c/td\u003e\u003ctd\u003e0.48\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e224\u003c/th\u003e\u003ctd\u003e0.30\u003c/td\u003e\u003ctd\u003e0.60\u003c/td\u003e\u003ctd\u003e0.52\u003c/td\u003e\u003ctd\u003e0.54\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e256\u003c/th\u003e\u003ctd\u003e0.33\u003c/td\u003e\u003ctd\u003e0.64\u003c/td\u003e\u003ctd\u003e0.57\u003c/td\u003e\u003ctd\u003e0.60\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e288\u003c/th\u003e\u003ctd\u003e0.37\u003c/td\u003e\u003ctd\u003e0.67\u003c/td\u003e\u003ctd\u003e0.62\u003c/td\u003e\u003ctd\u003e0.66\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e320\u003c/th\u003e\u003ctd\u003e0.40\u003c/td\u003e\u003ctd\u003e0.71\u003c/td\u003e\u003ctd\u003e0.68\u003c/td\u003e\u003ctd\u003e0.72\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e352\u003c/th\u003e\u003ctd\u003e0.44\u003c/td\u003e\u003ctd\u003e0.74\u003c/td\u003e\u003ctd\u003e0.73\u003c/td\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e384\u003c/th\u003e\u003ctd\u003e0.47\u003c/td\u003e\u003ctd\u003e0.77\u003c/td\u003e\u003ctd\u003e0.78\u003c/td\u003e\u003ctd\u003e0.84\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e416\u003c/th\u003e\u003ctd\u003e0.51\u003c/td\u003e\u003ctd\u003e0.81\u003c/td\u003e\u003ctd\u003e0.83\u003c/td\u003e\u003ctd\u003e0.90\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e448\u003c/th\u003e\u003ctd\u003e0.54\u003c/td\u003e\u003ctd\u003e0.84\u003c/td\u003e\u003ctd\u003e0.89\u003c/td\u003e\u003ctd\u003e0.96\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e480\u003c/th\u003e\u003ctd\u003e0.57\u003c/td\u003e\u003ctd\u003e0.88\u003c/td\u003e\u003ctd\u003e0.94\u003c/td\u003e\u003ctd\u003e1.02\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e512\u003c/th\u003e\u003ctd\u003e0.61\u003c/td\u003e\u003ctd\u003e0.91\u003c/td\u003e\u003ctd\u003e0.99\u003c/td\u003e\u003ctd\u003e1.08\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e544\u003c/th\u003e\u003ctd\u003e0.64\u003c/td\u003e\u003ctd\u003e0.94\u003c/td\u003e\u003ctd\u003e1.04\u003c/td\u003e\u003ctd\u003e1.14\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e576\u003c/th\u003e\u003ctd\u003e0.68\u003c/td\u003e\u003ctd\u003e0.98\u003c/td\u003e\u003ctd\u003e1.09\u003c/td\u003e\u003ctd\u003e1.20\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e608\u003c/th\u003e\u003ctd\u003e0.71\u003c/td\u003e\u003ctd\u003e1.01\u003c/td\u003e\u003ctd\u003e1.15\u003c/td\u003e\u003ctd\u003e1.26\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr class=\"used\"\u003e\u003cth\u003e640\u003c/th\u003e\u003ctd\u003e0.74\u003c/td\u003e\u003ctd\u003e1.03\u003c/td\u003e\u003ctd\u003e1.20\u003c/td\u003e\u003ctd\u003e1.32\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\tNumber of frames required to crossfade \u003cvar\u003eWidth\u003c/var\u003e×400 pixels onto all four planes of VRAM. Thanks to 1️⃣ オップナー2608, 2️⃣ Furball, 4️⃣ Will.Broke.It, and 5️⃣ spaztron64 for the real-hardware tests.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tNow \u003ci\u003ethat's\u003c/i\u003e what we want to see! A clear winner across all relevant generations of PC-98 hardware. Activating the EGC really is only costly on paper; it's worth it even if you just need to perform a single tall RMW blit across all bitplanes.\u003cbr\u003e\n\tMost importantly, though: \u003ci\u003eThis optimization was crucial for emulators, and absolutely worth it.\u003c/i\u003e At Neko Project's emulated 66\u0026nbsp;MHz, ZUN's code took consistently longer than one frame to crossfade 640×400 pixels onto VRAM. Hence, TH05's title screen animation always effectively rounded up that logical frame to 2 displayed frames. While this is still valid as per \u003ca href=\"/blog/2025-09-06#fp-2025-09-06\"\u003e📝 frame-perfection rule #2\u003c/a\u003e, it still counts as unintentional slowdown that we've now removed.\n\u003c/p\u003e\u003cp\u003e\n\tIn return, ZUN also made the right call by not bothering about this particular optimization. As image widths and CPU speeds increase, minimizing the number of EGC runs has increasingly diminishing returns.\u003cbr\u003e\n\tThese results might even suggest that we should just formalize a 2-frame delay because \u003ci\u003eclearly\u003c/i\u003e, no real-hardware model could ever overcome the VRAM latencies and actually crossfade a single 640×400 image within less than one frame. But again, such a 2-frame delay would be no more or less correct than the 3-frame delay on the PC-9821Na7, or the probably even higher delays on a 486 model. When in doubt, we just go by what ZUN \u003ci\u003ewrote\u003c/i\u003e. My allegiance lies with code, not with Japan's e-waste.\n\u003c/p\u003e\u003cp\u003e\n\tFinally, we can also see how upgrading the CPU of your real-hardware PC-98 does nothing to work around VRAM latencies – i.e., the one bottleneck that actually matters for PC-98 Touhou. You'd basically just burn all those additional CPU cycles on waiting for VRAM. Such a CPU upgrade might even be counter-productive, as we can see with the 533\u0026nbsp;MHz CPU getting outperformed by not only the 400\u0026nbsp;MHz AMD K6-III, but even a 133\u0026nbsp;MHz Pentium, due to what spaztron64 suspects to be a southbridge issue.\n\u003c/p\u003e\u003ch4 id=\"xfade-future-2025-09-10\"\u003eFuture work\u003c/h4\u003e\u003cp\u003e\n\tBut let's think ahead for a moment about what this means for the games as a whole. If the EGC is \u003ci\u003ethat\u003c/i\u003e good at this task compared to the CPU, how good could it possibly be for 16-color sprites with an alpha plane? master.lib's \u003ccode\u003esuper_*()\u003c/code\u003e functions blit these using GRCG\u0026nbsp;+ CPU techniques, which decisively lost this benchmark on ≥Pentium hardware and weren't that good on Neko Project either. In contrast, the EGC has a lot going for it, and not just because of these benchmark numbers:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWe get \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 blitting at non-byte-aligned X positions for basically free\u003c/a\u003e, without having to bit-shift on the CPU. Since our source data will always be byte-aligned, I don't even have to handle \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 the second case of (source bit address \u0026gt; destination bit address) that requires the tile register to be prefilled\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eSince we never cut any of these \u003ccode\u003esuper_*()\u003c/code\u003e sprites into subregions, we can always blast sprite data into VRAM with a single \u003ccode\u003eREP MOVSD\u003c/code\u003e instruction per bitplane. The VRAM representation as a contiguous 1D strip of bytes matches the way sprites are stored in RAM.\u003c/li\u003e\n\t\u003cli\u003eIf we treat the 768×4 offscreen bytes as a more sophisticated kind of sprite cache, we \u003ci\u003emight\u003c/i\u003e be able to further reduce those initial VRAM writes.\u003cbr\u003e\n\t(Probably not, though: There's a good chance that this would actually slow down the game due to the overhead of managing these very few cache bytes. At first, this idea might seem perfect for directional bullets: Since these typically come in multiples and at different angles, couldn't we just blit the 512×4 bytes for \u003ca href=\"/blog/2023-06-30\"\u003e📝 all 16 directional variants\u003c/a\u003e to the offscreen area once and then blit every bullet of that type at every possible direction without refilling the cache? However, the original game always renders 16×16 bullets in \u003ci\u003earray\u003c/i\u003e order, without grouping them by type first. We can't \u003ci\u003eadd\u003c/i\u003e grouping ourselves because it would violate our \u003ca href=\"/blog/2025-09-06#fp-2025-09-06\"\u003e📝 frame-perfection rules\u003c/a\u003e: If the game fires different 16×16 bullet types simultaneously, \u003ca href=\"https://youtu.be/Hjvg3LYVVRo?si=5SPHvrRgYgNLdLM6\u0026t=926\"\u003esuch as in these three patterns in the Mai \u0026amp; Yuki fight\u003c/a\u003e, we might change the rendering order and thus end up with a visible difference if two bullets of different types overlap. And since there's no bug here, I can't even justify changing this for the Anniversary Editions.)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIf this frees up enough performance, it might not \u003ci\u003ejust\u003c/i\u003e help with achieving 0% slowdown on real or emulated PC-98 systems. Maybe the games would then even run decently well on real 386 models, which might even shift the entire marketplace for PC-98 hardware in response? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Or maybe, we could even have a high-res mod of TH03 that removes \u003ca href=\"/blog/2022-02-18\"\u003e📝 SPRITE16 and its sprite area\u003c/a\u003e to run the in-game portion at twice its original vertical resolution with more detailed sprites, but without increasing real-hardware system requirements all too much? Looking forward to writing and running that benchmark in another 2 years or so…\n\u003c/p\u003e\u003chr id=\"pi-2025-09-10\"\u003e\u003cp\u003e\n\tFor now though, that's enough research into blitting performance. Now that we know the optimal way of getting big images from RAM to VRAM in any situation, how do we get them from disk to RAM in the first place? Also, wasn't that animation much slower than the 2 frames per fade step that we would expect from the benchmark results? What's going on there?\n\u003c/p\u003e\u003ch3\u003eOptimizing .PI image handling\u003c/h3\u003e\u003cp\u003e\n\tUncompressed 640×400 images are rather large, common hard disk capacities were just starting to hit single-digit GB ranges in the late 90s, and you wouldn't want to haul even more floppy disks to Comiket. So it made sense why ZUN wanted to store these images in compressed form. Enter PI, the second-most common losslessly compressed image format within the PC-98 scene of the 90s. Technically, it's \u003ca href=\"https://mooncore.eu/bunny/txt/pi-pic.htm\"\u003enot that complex\u003c/a\u003e, but it still achieves great compression ratios for ZUN's images:\n\u003c/p\u003e\u003cfigure class=\"ratios pi-2025-09-10\"\u003e\u003crec98-child-switcher\u003e\n\t\u003ctable data-title=\"Byte sizes\" class=\"active\"\u003e\n\t\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th02.png?c6bfc44e\" alt=\":th02:\" width=\"24\" height=\"24\" \u003e TH02\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e TH03\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e TH05\u003c/th\u003e\n\t\t\t\t\u003cth\u003eTotal\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/thead\u003e\n\t\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eUncompressed\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e 2,195,904\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 4,874,240\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 4,815,360\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 5,905,920\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e17,791,424\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003e.PI\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e  290,921\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  807,690\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  765,909\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  822,993\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e2,687,513\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e  254,074\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  660,020\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  635,537\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e  617,115\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e2,166,746\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\n\t\u003ctable data-title=\"Ratios\"\u003e\n\t\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th02.png?c6bfc44e\" alt=\":th02:\" width=\"24\" height=\"24\" \u003e TH02\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e TH03\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04\u003c/th\u003e\n\t\t\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e TH05\u003c/th\u003e\n\t\t\t\t\u003cth\u003eTotal\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/thead\u003e\n\t\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\u003cth\u003eUncompressed\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e100.00 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e100.00 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e100.00 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e100.00 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e100.00 %\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003e.PI\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e 13.25 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 16.57 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 15.91 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 13.94 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 14.91 %\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\u003cth\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/th\u003e\n\t\t\t\t\u003ctd\u003e 11.57 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 13.54 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 13.20 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 10.45 %\u003c/td\u003e\n\t\t\t\t\u003ctd\u003e 12.19 %\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\n\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003c/figure\u003e\u003cp\u003e\n\tHowever, these compression ratios come at an annoying price for the PC-98 in particular. The algorithm naturally decompresses images into a \u003ci\u003epacked\u003c/i\u003e representation with color values from 0 to 15 inclusive, \u003ci\u003enot\u003c/i\u003e into the planar 4×1bpp format required by the PC-98's 16-color graphics mode. It makes a lot of sense why PI is \u003ci\u003edesigned\u003c/i\u003e around packed pixels:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ePIC, PI's bigger sibling, originated on the Sharp X68000, \u003ca href=\"https://gamesx.com/wiki/doku.php?id=x68000:screen_control\"\u003ewhose graphics RAM uses a packed format as well\u003c/a\u003e. (By the way, that platform would also be the technically most interesting porting target for PC-98 Touhou, but that's a story for another day.)\u003c/li\u003e\n\t\u003cli\u003eIf a bitplane-centric compressor only looks at one bitplane at a time and doesn't correlate its bits with the other three planes, it's bound to compress the same shapes and image features up to 4 times, depending on how many palette index bits are involved in creating them. Optimally compressing individual bitplanes therefore turns into an exercise of isolating these features within as few bitplanes as possible, which doesn't always work for every image. This \u003ci\u003ecould\u003c/i\u003e work for standalone images where the encoder is free to optimize the palette and  image bits. But game assets must typically conform to a predefined palette because of other sprites or even game code that already references certain colors within that palette.\u003cbr\u003e\n\tBy focusing on packed colors, PI gets to implement a \u003ca href=\"https://mooncore.eu/bunny/txt/pi-pic.htm\"\u003emove-to-front transform\u003c/a\u003e that easily manages a similar kind of compression regardless of specific bit values. If the next color to be encoded has been recently seen next to the previously encoded color, that next color can then be encoded in just 2 bits rather than 4. Combine this delta encoding with a way of copying previously encoded pixel strips of arbitrary length, and you've got a great match for the often heavily dithered images you'd want to use on a PC-98.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut why, then, does master.lib only provide \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e, which returns the loaded image in the same packed format that the decompressor naturally produces? I've been wondering that ever since I \u003ca href=\"/blog/2022-11-30#games-2022-11-30\"\u003e📝 decompiled the TH03 cutscene system\u003c/a\u003e, where the performance issues of this packed format became obvious. This approach only makes sense for the rare use case of using 16-color images as an input for some algorithm and not actually displaying them on screen. But since people \u003ci\u003edo\u003c/i\u003e want to display .PI images, master.lib had to provide a second function for actually rendering these packed pixel buffers \u003ci\u003eanyway\u003c/i\u003e. Just look at the sheer amount of work that \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e has to do to unpack just 8 pixels into the 4×1 bytes for every bitplane, every time you want to write them to VRAM:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre style=\"height: 15em\"\u003emov\tCL, 2\n\nmov\tBL, [SI]\nmov\tBH, 0\nshl\tBX, CL\nmov\tAX, word ptr CS:RotTbl[BX]\nmov\tDX, word ptr CS:RotTbl[BX + 2]\ninc\tSI\nshl\tAX, CL\nshl\tDX, CL\nmov\tBL, [SI]\nmov\tBH, 0\nshl\tBX, CL\nor \tAX, word ptr CS:RotTbl[BX]\nor \tDX, word ptr CS:RotTbl[BX + 2]\ninc\tSI\nshl\tAX, CL\nshl\tDX, CL\nmov\tBL, [SI]\nmov\tBH, 0\nshl\tBX, CL\nor \tAX, word ptr CS:RotTbl[BX]\nor \tDX, word ptr CS:RotTbl[BX + 2]\ninc\tSI\nshl\tAX, CL\nshl\tDX, CL\nmov\tBL, [SI]\nmov\tBH, 0\nshl\tBX, CL\nor \tAX, word ptr CS:RotTbl[BX]\nor \tDX, word ptr CS:RotTbl[BX + 2]\ninc\tSI\n\nmov\tES:[DI], AL      \t; 0a800h\nmov\tBX, ES\nmov\tES:[DI+8000h], AH\t; 0b000h\nadd\tBH, 10h\nmov\tES, BX\nmov\tES:[DI], DL       \t; 0b800h\nadd\tBH, 28h\nmov\tES, BX\nmov\tES:[DI], DH       \t; 0e000h\nsub\tBH, 38h\nmov\tES, BX\ninc\tDI\u003c/pre\u003e\n\t\u003cfigcaption\u003eDon't try to understand this code, just look at the sheer \u003ci\u003eamount\u003c/i\u003e of instructions. Ideally, we want to replace four of these loop bodies with one \u003ccode\u003eMOVSD\u003c/code\u003e per bitplane, or just blit an entire bitplane with a single \u003ccode\u003eREP MOVSD\u003c/code\u003e for 640-wide images.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis algorithm costs \u0026gt;81 cycles on a 486 and \u0026gt;141 cycles on a 386, \u003ci\u003efor every 8 pixels\u003c/i\u003e. Multiply this by the 256,000 pixels of a single 640×400 image, and you get \u003ci\u003eup to 5 frames\u003c/i\u003e on Neko Project's 66\u0026nbsp;MHz CPU for blitting \u003ci\u003eany\u003c/i\u003e full-screen background image.\u003cbr\u003e\n\tThere is an undocumented \u003ccode\u003egraph_pi_load_\u003cstrong\u003eun\u003c/strong\u003epack()\u003c/code\u003e function, but that one \"unpacks\" a .PI into an utterly wasteful and unhelpful packed \u003ci\u003e8-bit\u003c/i\u003e format, and so is probably undocumented for a reason.\n\u003c/p\u003e\u003cp\u003e\n\tOK, but if performance is bad, there must be some really good other arguments in favor of this packed representation. Right?\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ePI's sequence repetition mechanism requires access to previously decoded pixels. Copying them around within a single packed buffer is just way faster than masking and reassembling bits from four different buffers.\u003c/li\u003e\n\t\u003cli\u003eKeeping \u003ci\u003eboth\u003c/i\u003e packed and planar versions of the same image in heap memory during decoding becomes increasingly prohibitive in Real Mode as image sizes increase.\u003c/li\u003e\n\t\u003cli\u003eAlso, we can support images whose widths are multiples of 2 rather than 8!\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tErr, nope. If you look at \u003ca href=\"https://mooncore.eu/bunny/txt/pi-pic.htm\"\u003ePI's location codes\u003c/a\u003e, you'll see that they can only reach up to two rows back from the current cursor position. Hence, we only ever need to keep a three-row window of packed pixels – the current row, and the two previous ones – which would slide down as the image gets decoded. At the end of each row, we then convert the third row to planar pixels, write them to our result buffer, and slide down the window by shifting up the bottom two packed rows, making room for the next row. Sure, this means that we end up shifting ((\u003csup\u003e\u003cvar\u003ewidth\u003c/var\u003e\u0026nbsp;× \u003cvar\u003eheight\u003c/var\u003e\u003c/sup\u003e/\u003csub class=\"hovertext\" title=\"Each byte contains 2 4-bit pixels\"\u003e2\u003c/sub\u003e)\u0026nbsp;× \u003cspan class=\"hovertext\" title=\"Number of backreference lines shifted up for each row\"\u003e2\u003c/span\u003e) more bytes around in memory. But copies are fast, and the first blit of a resulting 640×400 planar image would still be faster than \u0026gt;5 frames even with these copies factored in.\u003cbr\u003e\n\tAlso, the width restriction is not really an argument if you consider that \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e already had the same limitation. Consequently, all of ZUN's .PI images already fulfill that requirement.\n\u003c/p\u003e\u003chr id=\"pi-attempts-2025-09-10\"\u003e\u003cp\u003e\n\tStill, 23 frames to decode a single 640×400 image, using a function that \u003ca href=\"https://github.com/koizuka/master.lib/blob/c4a5e54116656681fc9449a363528dcf5a60ef05/src/grppilod.asm#L73\"\u003erestricts itself to 80186-level instructions\u003c/a\u003e? Surely we can do better if we target ≥386 CPUs anyway? I've also been on a \u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 mission to remove master.lib and any ASM translation units in the debloated/portable branch\u003c/a\u003e, so let's write a new .PI loader in C++!\u003cbr\u003e\n\tUnfortunately, it quickly becomes obvious why you want to write this sort of unpacker in ASM. If we look back at the current record holder for the most unoptimized ZUN-written code, \u003ca href=\"/blog/2024-11-22#perf-2024-11-22\"\u003e📝 the curve animation in TH03's character selection screen\u003c/a\u003e primarily wastes CPU cycles because the sheer number of 4,096 points per logical frame magnifies the impact of every suboptimal code generation detail. Now increase that to the roughly \u003ci\u003e100,000\u003c/i\u003e decoding operations required by a moderately complex 640×400 .PI file, and your C++ code just can't possibly compete with any well-written ASM implementation. There isn't much use for 32-bit instructions in PI decoding either, since it mostly comes down to making decisions based on individual \u003ci\u003ebits\u003c/i\u003e. You'd rather use the 4 main general-purpose registers (\u003ccode\u003eAX\u003c/code\u003e, \u003ccode\u003eBX\u003c/code\u003e, \u003ccode\u003eCX\u003c/code\u003e, and \u003ccode\u003eDX\u003c/code\u003e) as 8 8-bit registers rather than 4 32-bit ones. And not only are C compilers of that era bad at allocating registers, but we would also have to constantly fight C's \u003ca href=\"/blog/2024-10-22#cleanup-2024-10-22\"\u003e📝 integer\u003c/a\u003e \u003ca href=\"/blog/2024-12-04#limit-2024-12-04\"\u003e📝 promotion\u003c/a\u003e \u003ca href=\"/blog/2025-08-12#bug-2025-08-12\"\u003e📝 rules\u003c/a\u003e that will prevent us from using 8-bit registers at every point.\u003cbr\u003e\n\tAnd so, my initial attempt at a C++ version was 3.3× slower than the combination of master.lib's \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e and \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e. Even after deploying every inline ASM trick I came up with, I could only bring down that number to 1.7×. There goes the dream of using 100% C++ and no ASM in the codebase for PC-98 Touhou. Might as well continue using all the good parts of master.lib then…\n\u003c/p\u003e\u003cp\u003e\n\tOn to plan B then, forking \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e to immediately unpack the image into four bitplanes. Integrating the sliding-window algorithm into plain ASM code was slightly tricky, but not the worst thing in the world.\u003cbr\u003e\n\tIt did reveal another slight drawback with the general approach of decoding a PI image into a single output buffer, though. PI supports backreferences to the two previous rows even \u003ci\u003ewithin\u003c/i\u003e the first two rows of an image, where you'd expect them to point outside of the image and thus be invalid. In this case, the algorithm expects the two rows \"above\" the image to be flood-filled with the 2×1-pixel pair in the top-left corner of the image. This also means that every PI bitstream must start by explicitly specifying these colors in PI's delta encoding. master.lib implements this requirement by always allocating the image buffer with two additional rows at the top, which remain allocated once that buffer is returned. I probably don't even have to explain how the sliding-window algorithm naturally solves this issue as well. Thus, we get to reduce the size of the returned buffer to exactly the size of the image and save a few bytes on the heap.\n\u003c/p\u003e\u003cp\u003e\n\tAs expected, this change merges the execution time of \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e into the execution time of \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e, and even manages to be slightly shorter despite all the added memory copies.\u003cbr\u003e\n\tBut now that this works, it would be interesting to compare the performance against the .PI library that ZUN used for TH01:\n\u003c/p\u003e\u003ch4 id=\"pi-piload-2025-09-10\"\u003ePiLoad\u003c/h4\u003e\u003cp\u003e\n\tIt's obvious why ZUN moved away from this library for the later four games. Despite the \u003ci\u003eLoad\u003c/i\u003e in its name, PiLoad exclusively decodes directly to VRAM, without ever writing the full planar image to conventional memory. This prevents any of the more dynamic use cases that ZUN had in mind for TH02: The multiple and quickly alternating \u003cspan lang=\"ja\"\u003e東方封魔録\u003c/span\u003e images in the title screen animation, the darkening/highlighting effect in the character selection, and the in-game bomb backgrounds all require decompressed image buffers in RAM to run at anywhere close to acceptable speeds at 66\u0026nbsp;MHz.\u003cbr\u003e\n\tBesides, PiLoad also restricts itself to 80186-level instructions, but looks like a giant mess. Self-modifying code? A packed→planar conversion that doesn't even use lookup tables, which \u003ca href=\"https://github.com/koizuka/master.lib/blob/c4a5e54116656681fc9449a363528dcf5a60ef05/src/grppput8.asm#L62\"\u003emaster.lib probably uses by default for good reasons\u003c/a\u003e? Sure seems as if PiLoad is heavily micro-optimized for the very first generations of PC-98 models that typically wouldn't even have enough RAM for such an image…\n\u003c/p\u003e\u003cp\u003e\n\t…except that all this micro-optimization makes PiLoad more than 2× as fast as master.lib's PI functions, even on Neko Project's emulated i386?!\u003cbr\u003e\n\tYup. If you look closer, PiLoad is nothing short of a masterclass in x86 optimization, applied to a use case where every cycle actually matters:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003ePiLoad internally decompresses into an 8-bit format that keeps the color in the top 4 bits and leaves the bottom 4 bits zeroed. While this wastes quite a bit of buffer space on the surface, it allows a few key optimizations:\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003e\u003cp\u003eSince every pixel is now byte-aligned, PiLoad can handle every possible type of sequence repetition with a simple \u003ccode\u003eREP MOVSB\u003c/code\u003e instruction. This is especially handy for the \u003ccode\u003e110\u003c/code\u003e and \u003ccode\u003e111\u003c/code\u003e location codes, which refer to a source block of pixels that starts either (\u003cvar\u003ewidth\u003c/var\u003e\u0026nbsp;- 1) or (\u003cvar\u003ewidth\u003c/var\u003e\u0026nbsp;+ 1) pixels before the current write cursor. With a 4bpp in-memory format, you additionally have to handle the slower case you'd run into 50% of the time, where the source block starts in the middle of a byte and forces you to manually extract and recombine nibbles.\u003c/p\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003cp\u003ePiLoad can then optimally place the color table for the move-to-front transform in the \"zero page\" of the decode buffer's segment, from offsets \u003ccode\u003e0x00\u003c/code\u003e to \u003ccode\u003e0xFF\u003c/code\u003e inclusive. Then, each 8-bit color value doubles as the address to its corresponding table row, removing any need for further address calculations. 🤯\u003c/p\u003e\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eBit reading is incredibly efficient, making use of the key insight that early x86 favors non-taken jumps over taken ones. If you consumed all bits in your current byte-sized input data shift register, you must advance to the next byte in your file buffer and potentially reload bytes from disk after you reach the end of the buffer. Optimally, you'd want to optimize for the happy path you will hit in 7 out of 8 cases: decrement the bit counter (1 instruction), jump away to buffer refill code if necessary (1 instruction), read the bit into the carry flag (1 instruction), and then immediately process the value of the carry flag in the next instruction. But how do you conditionally \"jump away\" and then return to where you came from? x86 only has conditional \u003ci\u003ejumps\u003c/i\u003e and no conditional \u003ci\u003ecalls\u003c/i\u003e, and even if it did, calls would add even more cycles of latency. This means that you can't just define a single bit reader function and then inline it, because such a function would always look something like this:\n\t\u003cfigure\u003e\u003cpre\u003e\tdec\tbits_remaining\n\tjnz\t@@buffer_still_contains_bits\n\n@@refill:\n\t; Load [bits] from buffer or disk…\n\t; …\n\n\tmov\tbits_remaining, 8\n\n@@buffer_still_contains_bits:\n\tshl\tbits, 1\t; Shift next bit into carry flag\u003c/pre\u003e\u003c/figure\u003e\n\tThe \u003ccode\u003e@@refill\u003c/code\u003e code necessarily has to be part of the function, but awkwardly sits between the check and the call-site usage code. Execution would have to jump over it in the majority of cases, which is the opposite of what we want. This is what \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e does, and it wastes \u0026gt;14 cycles per compressed .PI input byte on a 486, \u0026gt;35 cycles on a 386 or 286, \u0026gt;63 cycles on a 186, and \u0026gt;84 cycles on an 8086.\u003cbr\u003e\n\tInstead, PiLoad instantiates a separate copy of its \u003ccode\u003e@@refill\u003c/code\u003e code for every one of the 22 instances where the decoder needs to read a bit. This way, each bit reader instance can directly jump back to the happy path of its usage code:\n\t\u003cfigure\u003e\u003cpre\u003e\tdec\tbits_remaining\n\tjz\trefill_instance_0\nhappy_path_0:\n\tadd\tbit_reg, bit_reg\t; Shift next bit into carry flag\n\t; Use the carry flag…\n\t; …\n\nrefill_instance_0:\n\t; Load [bit_reg] from buffer or disk…\n\t; …\n\tmov\tbits_remaining, 8\n\tjmp\thappy_path_0\u003c/pre\u003e\u003c/figure\u003e\n\tThis kind of optimization even reaches into the physical layout of the code. On the 80186, conditional jumps were still limited to ±128 bytes from the current position. Therefore, PiLoad can't just bunch all 22 instances into a single place, but must place each instance as close as possible to its usage code, at a place where regular execution jumps over a large part of code anyway.\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eFinally, the packed→planar conversion is done by… alternating \u003ccode\u003eADD\u003c/code\u003e and \u003ccode\u003eADC\u003c/code\u003e instructions?!\u003c/p\u003e\n\t\u003cfigure\u003e\u003cpre\u003e\n; Each iteration of this loop consumes two pixels and thus writes two output bits per bitplane.\n; Thus, looping 4 times gives us a full byte for each bitplane.\nrept 4\n\t; Note how processing 2 pixels / 16 bits is the most natural solution here, even on a 32-bit\n\t; system. The algorithm relies on each decoded packed pixel byte being independently\n\t; accessible, and x86 only gives you this access to the first and second byte of its AX, BX,\n\t; CX, and DX registers. Hence, this instruction loads the next two pixels into AL (left) and AH\n\t; (right), due to x86 being little-endian.\n\t; On 32-bit CPUs, `LODSD` followed by a `SHR EAX, 16` further down would be faster on paper,\n\t; but the actual impact is something we'd have to benchmark.\n\tlodsw\n\n\t; Shift out each successive color bit into the carry flag, and from there into one of our four\n\t; target 8-bit registers (DH, DL, BH, BL) that represent the next 8 pixels on each bitplane.\n\t; Since we have to look at the upper 4 bits of AL and AH, left-shifting via addition gives us\n\t; the color bits in order from most to least significant, so we also have to shift them into\n\t; the bitplane registers in this order.\n\t; Another neat little detail: We don't even have to clear the target registers before this\n\t; unrolled loop! After 4 iterations, we will have rotated 8 new bits into each of our four\n\t; 8-bit target registers, which will have automatically shifted out any of their previous bits.\n\tadd\tal, al\t; Left\n\tadc\tbl, bl\t; Plane 3\n\tadd\tah, ah\t; Right\n\tadc\tbl, bl\t; Plane 3\n\tadd\tal, al\t; Left\n\tadc\tbh, bh\t; Plane 2\n\tadd\tah, ah\t; Right\n\tadc\tbh, bh\t; Plane 2\n\tadd\tal, al\t; Left\n\tadc\tdl, dl\t; Plane 1\n\tadd\tah, ah\t; Right\n\tadc\tdl, dl\t; Plane 1\n\tadd\tal, al\t; Left\n\tadc\tdh, dh\t; Plane 0\n\tadd\tah, ah\t; Right\n\tadc\tdh, dh\t; Plane 0\nendm\u003c/pre\u003e\n\t\t\u003cfigcaption\u003eA lot simpler than what you'd apparently have to do on an Amiga, as revealed by a Google search for \u003ckbd\u003echunky to planar\u003c/kbd\u003e. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\tIt seems like BERO was just as surprised about this equivalence between \u003ccode\u003eADD/ADC reg, reg\u003c/code\u003e and \u003ccode\u003eSHL/RCL reg, 1\u003c/code\u003e as I was, and probably defined the \u003ca href=\"https://github.com/nmlgc/ReC98/blob/9e3869341542d9fa8220056c972bf709a989bd54/libs/piloadc/piloadc.asm#L18-L23\"\u003e\u003ccode\u003eshl1\u003c/code\u003e and \u003ccode\u003ercl1\u003c/code\u003e macros\u003c/a\u003e used by the actual code to improve clarity.\u003cbr\u003e\n\tComparing the \u003ca href=\"https://www2.math.uni-wuppertal.de/~fpf/Uebungen/GdR-SS02/opcode_i.html\"\u003ecycle counts\u003c/a\u003e of the two variants then reveals quite a surprise:\n\t\u003cfigure\u003e\n\t\t\u003ctable id=\"cycles-2025-09-10\"\u003e\n\t\t\t\u003cthead\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e8086\u003c/th\u003e\u003cth\u003e80186\u003c/th\u003e\u003cth\u003e80286\u003c/th\u003e\u003cth\u003e80386\u003c/th\u003e\u003cth\u003e80486\u003c/th\u003e\n\t\t\t\t\u003c/tr\u003e\n\t\t\t\u003c/thead\u003e\n\t\t\t\u003ctbody\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003eADD/ADC reg, reg\u003c/code\u003e\u003c/th\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\u003ctd\u003e3\u003c/td\u003e\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003eSHL/ROL/RCL reg, 1\u003c/code\u003e\u003c/th\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e3\u003c/td\u003e\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\n\t\t\t\u003c/tbody\u003e\n\t\t\u003c/table\u003e\n\t\t\u003cfigcaption\u003eWithout Pentium numbers, as we've seen above that Pentium systems are bottlenecked by the GDC, not the CPU.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cp\u003e\n\t\tYup. PiLoad's choice of instructions tells us that it was indeed deliberately optimized for ≥386 CPUs, the exact ones you'd want to optimize PC-98 Touhou code for, despite formally restricting itself to the 80186 ISA. We don't know whether Koizuka deliberately wanted to target ≤286 CPUs with \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e, but the fact remains that this choice slowed down PC-98 Touhou on its target hardware. And yes, this includes the table-less version: While that one uses the exact same \u003ci\u003ealgorithm\u003c/i\u003e as PiLoad, it spells \u003ccode\u003eADD\u003c/code\u003e and \u003ccode\u003eADC\u003c/code\u003e as \u003ccode\u003eROL 1\u003c/code\u003e and \u003ccode\u003eRCL 1\u003c/code\u003e and thus only runs minimally faster than the table-driven version on Neko Project's emulated i386.\n\t\u003c/p\u003e\n\t\u003cli\u003eDespite all the inlining, PiLoad still ends up 207 bytes \u003ci\u003esmaller\u003c/i\u003e than the combination of master.lib's \u003ccode\u003egraph_pi_load_pack()\u003c/code\u003e and the default version of \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e with its 1,024-byte lookup table.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo, let's add decoding into memory to PiLoad instead! Turns out that PiLoad was the simpler library to extend all along, since it already used the same sliding-window technique for unpacking pixels into VRAM. All it needed was a width check for a multiple of 8, a calculation of the bitplane size, and a new and much simpler unpacking function that doesn't need to support unaligned X positions or GRCG-powered transparency.\u003cbr\u003e\n\tHowever, \u003ca href=\"/blog/2025-09-29#fragment-2025-09-29\"\u003e📝 a certain unfortunate issue that will crop up in part 4\u003c/a\u003e will require more outside control over the loading process. As a result, the API of my forked version looks quite different:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eI separated the loading process into a \u003ci\u003eheader loading\u003c/i\u003e and \u003ci\u003ebitplane decoding\u003c/i\u003e step, giving the caller full control over how and where to allocate the final image buffer.\u003c/li\u003e\n\t\u003cli\u003eI replaced all of PiLoad's own \u003ccode\u003eINT 21h\u003c/code\u003e file reading code with a single read callback, shifting the responsibility for opening and closing .PI files to the caller. master.lib's \u003ccode\u003eINT 21h\u003c/code\u003e-hooking packfile feature would later turn out to be one of the most poorly-implemented and overly complex features I've seen so far within that library, perhaps even worse than packed .PI loading. Hooking \u003ccode\u003eINT 21h\u003c/code\u003e was the obvious choice for easily supporting optional packfiles in both master.lib's own code and any third-party libraries, but the cost this comes at…\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn exchange for the new features, I decided to drop a few features that we either don't need in this codebase or that are better implemented at another level. This list includes\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/13629ddd6c7702450e0ef77c2169516ac7c1d148\"\u003edirect blitting at unaligned X positions\u003c/a\u003e (not needed),\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/6d506155272f5942a26e21ad0c26280bd0043a5c\"\u003esupport for using the library from Pascal\u003c/a\u003e (sorry),\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/89075413fe085f79243811bf886281192b0d182c\"\u003epalette setting and toning\u003c/a\u003e (covered by master.lib),\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/09c367362ebdf9b8d7bb3677d44d67273f44b039\"\u003evideo mode setting\u003c/a\u003e (covered by master.lib), and\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/422c985396b54bef332e91c9f21e4a8f17d2f47c\"\u003ecomment display\u003c/a\u003e (not needed).\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis might not be ideal for other PC-98 developers who might want to use my fork in their projects, but you can always revert these commits in your own version.\n\u003c/p\u003e\u003chr id=\"pi-th03-2025-09-10\"\u003e\u003cp\u003e\n\tBefore we can entirely replace master.lib's PI functions with PiLoad in TH02-TH05 though, we have to address one particular silly detail:\n\u003c/p\u003e\u003ch4\u003e\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e TH03's silly line-doubled sprite sheets \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/h4\u003e\u003cp\u003e\n\tAs we all know by now, \u003ca href=\"/blog/2022-02-18\"\u003e📝 TH03 renders its in-game graphics at line-doubled 640×200 to free up half of VRAM for sprite data\u003c/a\u003e. This means that it can store all sprites at half of their displayed vertical resolution, saving some disk space and RAM in the process. However, ZUN only \u003ci\u003eactually\u003c/i\u003e did this for the uncompressed sprites loaded from the .BF2 and \u003ca href=\"/blog/2020-11-16\"\u003e📝 .MRS\u003c/a\u003e files. All in-game .PI sprite sheets are, for some reason, stored in the same line-doubled form that would be shown on screen:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 640px;\"\u003e\n\t\u003cimg src=\"/blog/static/2025-09-10-TH03-ENEMY01.PI.webp?e24b0da6\" width=\"640\" alt=\"TH03's ENEMY01.PI, showing off the unnecessary line doubling of TH03's in-game sprites loaded from .PI files\"\u003e\n\t\u003cfigcaption\u003e\n\t\tSince PI's location codes are designed around vertical repetition, line doubling has a negligible impact on compressed file sizes. Such an image still decodes into twice as many bytes as needed, though…\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis is very silly because your PI implementation now needs a feature to undo this doubling by skipping over every second line at either load or render time. ZUN chose to place this feature at render time because that's the simplest solution when you use master.lib's PI functions. Since \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e unpacks and blits only one line at a time, it already expects the caller to loop over all lines of a PI image and to do all the x86 segment arithmetic required for handling images larger than 64\u0026nbsp;KiB. From there, it's only natural to add a copy of that function that skips every second line.\u003cbr\u003e\n\tBut such a render-time feature would strain our generic blitter:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAt worst, we'd have a \u003ci\u003e\"are we line-skipping\"\u003c/i\u003e check and branch within the low-level blitter functions, at the end of every row. Such a branch would add new code for the line-skipping case that would be jumped over in the optimal case, slowing down the common case in the one place where we want to go fast.\u003c/li\u003e\n\t\u003cli\u003eWe could generate separate line-skipping versions for all low-level blitters, but that adds code bloat, blitter management bloat, \u003ci\u003eand\u003c/i\u003e API surface area bloat.\u003c/li\u003e\n\t\u003cli\u003eWe could adapt master.lib's solution and have the high-level draw calls only run the blitter for each 1-pixel row, which allows them to jump over every second line just like you would do with \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e. But conceptually, this just feels so \u003ci\u003ewrong\u003c/i\u003e. I designed the entire blitter to be fast for sprites with arbitrary height, even going so far as to unroll the usual vertical loop for images shorter than 192 pixels on the X axis, and then we're slamming the brakes just to work around these silly assets. Plus, the added API surface is still a burden for regular draw calls.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tMoving the skip to load time, on the other hand, has several clear advantages:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe loaded images \u003ci\u003eactually\u003c/i\u003e only require half the memory.\u003c/li\u003e\n\t\u003cli\u003eThey also decode slightly faster since we can skip the packed→planar conversion for every second line.\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/1e28b16861c2e7b4dddb7b49e49ddd8bc0e99090\"\u003eThe whole feature only requires 12 additional ASM instructions\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eWe'd still have to add a flag to the load call. But that's a one-time API surface cost, not a multiplicative one, and thus doesn't hurt at all.\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"goal-2025-09-10\"\u003e\u003cp\u003e\n\tAnd if we now put all of this together, we indeed get TH05's crossfaded title screen animation running at its intended speed on a 66\u0026nbsp;MHz Neko Project. Mission accomplished!\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-TH05-Title-animation-original-66-MHz.webp?1df72cb9\" preload=\"none\" controls data-title=\"Original game\" width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"129\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-original-66-MHz.avi?f90a7dbe\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-TH05-Title-animation-original-66-MHz.webm?0b48ec90\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-TH05-Title-animation-original-66-MHz.webm?d21bdffa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-TH05-Title-animation-original-66-MHz.webm?bfd23615\" type=\"video/webm\"\u003eVideo of TH05's title screen crossfading effect, as rendered by the original game on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. Showcases how slow .PI blitting and crossfading adds a whole 60 frames of lag to the animation. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-original-66-MHz.avi?f90a7dbe\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"27\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"50\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"66\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"82\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"98\" data-title=\"Full picture\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-10-TH05-Title-animation-fixed-66-MHz.webp?1df72cb9\" preload=\"none\" controls data-title=\"Debloated P0323 build\" data-active width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"129\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-fixed-66-MHz.avi?6c25d496\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-10-TH05-Title-animation-fixed-66-MHz.webm?cad09534\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-10-TH05-Title-animation-fixed-66-MHz.webm?df94be2e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-10-TH05-Title-animation-fixed-66-MHz.webm?3d81366a\" type=\"video/webm\"\u003eVideo of TH05's title screen crossfading effect, as rendered by the debloated P0323 build on Neko Project 21/W with a clock speed of 2.4576 × 27 = 66.3552 MHz. Showcases how ReC98's performance improvements now allow even 486 CPUs to run this animation at the speed ZUN specified in the code. \u003ca href=\"/blog/static/video/zmbv/2025-09-10-TH05-Title-animation-fixed-66-MHz.avi?6c25d496\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"22\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"27\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"31\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"35\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"38\" data-title=\"Full picture\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tInterestingly though, running this animation at its denoted speed now reveals a page-flipping bug in ZUN's code. Each crossfading step is supposed to be visible for 4 logical frames, but that's not what we get:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFor some reason, ZUN goes single-buffered before the first logical frame of the first mask pattern, by both showing and accessing page 0. The recording from the original game demonstrates how ZUN's code hilariously fails to race the beam here, as the combination of \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e and unoptimized crossfaded blitting stretches this single high-level draw call to 7 frames. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eZUN then continues double-buffering after this first frame, accessing page 1 and showing page 0 for the second frame of the first mask pattern. However, we've just seen page 0 on the frame before, so we're now spending a second logical frame on the same page.\u003c/li\u003e\n\t\u003cli\u003eThe rest of the animation is properly double-buffered. Each fade step is (wastefully) rendered twice, or once for each page, providing an example of \u003ca href=\"/blog/2025-09-06#pages-2025-09-06\"\u003e📝 the hardware detail leak I mentioned in part 1\u003c/a\u003e. But since the very first frame got duplicated, we've shifted the entire timing of the animation back by one logical frame.\u003c/li\u003e\n\t\u003cli\u003eAfter the code rendered the 4\u003csup\u003eth\u003c/sup\u003e logical frame of the 4\u003csup\u003eth\u003c/sup\u003e mask pattern, ZUN immediately returns to single-buffering on page 0 for the main menu, as the original game needs to use page 1 to store the raw background image for cursor and text unblitting purposes. This creates a more unusual landmine:\u003cul\u003e\n\t\t\u003cli\u003e\u003cp\u003e\n\t\t\tThe original game does first blit the final unmasked image to page 1, but then copies it from page 1 to page 0 using master.lib's notoriously slow \u003ccode\u003egraph_copy_page()\u003c/code\u003e function \u003ci\u003ewhile showing page 0\u003c/i\u003e. \u003ccode\u003egraph_copy_page()\u003c/code\u003e fully copies each bitplane before moving on to the next one, which explains the intermediate result on frame 97: Some colors of the title screen image already appear unmasked because their palette indices only use the lowest two bits whose planes have already been fully copied, the other colors get a discolored version of the fourth mask pattern, and we get a screen tearing line at the 371\u003csup\u003est\u003c/sup\u003e row of VRAM which reveals that master.lib only managed to copy 2.9275 out of the 4 bitplanes within this physical frame.\u003cbr\u003e\n\t\t\tHence, ZUN wasn't effectively double-buffering at all, despite the page flip.\n\t\t\u003c/p\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003cp\u003e\n\t\t\tSince the debloated build retains the background image in RAM, it doesn't need to blit anything to page 1. Therefore, it can go fully single-buffered and start blasting planar pixels from RAM to VRAM at some point near the start of the vertical blanking interval. Mysteriously, we manage to race the beam on every 66\u0026nbsp;MHz configuration, despite also fully blitting each 640×400 bitplane in order. 😲\u003cbr\u003e\n\t\t\u003c/p\u003e\u003c/li\u003e\n\t\u003c/ul\u003e\n\tIn both cases, the sudden flip to single-buffering cuts the last double-buffered frame from the crossfade animation that we were still supposed to show. Thus, the code defines a 5-4-4-3 sequence of logical frames for each fade step instead of the intended 4-4-4-4 one.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSee? And that's just one example of \u003ca href=\"/blog/2025-09-06#pages-2025-09-06\"\u003e📝 the bad code quality you invite by having page flips in high-level menu code\u003c/a\u003e. Now multiply that by the number of screen tearing landmines, and you'll get why this has taken so long.\n\u003c/p\u003e\u003cp\u003e\n\tAnother minor reason: Some of the code I wanted to debloat or make more portable within these 11 pushes was still in ASM, and it would have been way more annoying to keep and work around that code than to just decompile it. Next up: Taking a very quick look at these few decompilations.\n\u003c/p\u003e\u003cp\u003e\n\tOh, and just in case you want to run these benchmarks yourself:\n\t\u003ca class=\"download\" href=\"/blog/static/2025-09-10-benchmarks.zip?f7f047c9\" data-kb=\"126.0\"\u003e2025-09-10-benchmarks.zip \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-09-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-09-10T23:56:09Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-09-06",
      "url": "https://rec98.nmlgc.net/blog/2025-09-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-08-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-09-06\"\u003e\u003ctime datetime=\"2025-09-06T03:01:24Z\"\u003e2025-09-06 03:01\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0313\"\u003eP0313\u003c/a\u003e\n\t\t\tDebloating (Build system preparations / TH01 cleanup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f6d836b...04724c9\"\u003e\u003c/a\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ebb920b...e1270ee\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dosbox-x\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A DOSBox fork with support for PC-98 emulation.\"\u003edosbox-x\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/input\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Processing data entered from the keyboard or a joypad.\"\u003einput\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\u003cp\u003e\n\tTalk about a nerd snipe! I \u003ci\u003ejust\u003c/i\u003e wanted to take the first meaningful step towards getting PC-98 Touhou portable. But then, that step massively escalated and resulted in not only the single biggest subproject of 2025, but also in the most productive dev cycle this project has seen since the beginning of the crowdfunding era. 405 commits over 11 pushes, and touching on so many topics that writing a single blog post would have been way too much for even me to handle. So let's try something new and split this delivery into four \"smaller\" and thematically more focused posts that I'll release in quick succession:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ePart 1 (this post) describes the various strategies of porting PC-98 Touhou to modern platforms, explains which one I'm going to take and why, and clears up common misconceptions surrounding performance and accuracy. This one is required reading for anyone (yes, \u003ci\u003eanyone\u003c/i\u003e) who believes they want to see these games ported. Hence, it's also intended for people who aren't that familiar with ReC98 and its usual ideals, and tries to not go all too far into technical detail. (Hopefully.)\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/2025-09-10\"\u003e📝 Part 2\u003c/a\u003e will continue my \u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 investigation into PC-98 blitting performance\u003c/a\u003e and figure out how we can get the PC-98 versions closer to the ideals described in Part 1.\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/2025-09-16\"\u003e📝 Part 3\u003c/a\u003e will cover the few decompilations I needed to do in preparation for…\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/2025-09-29\"\u003e📝 Part 4\u003c/a\u003e, which will cover the actual set of changes I made to all the games.\u003c/li\u003e\n\u003c/ul\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#strategy-2025-09-06\"\u003eDeciding on a porting strategy\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#slowdown-2025-09-06\"\u003e\u003cq\u003eAccurate slowdown\u003c/q\u003e\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#config-2025-09-06\"\u003ePicking a CPU clock speed for emulators\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#66-2025-09-06\"\u003eCan 66\u0026nbsp;MHz be enough for anybody?\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#fp-2025-09-06\"\u003e\u003cq\u003eFrame-perfect\u003c/q\u003e\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#solve-2025-09-06\"\u003eResolving screen tearing\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#thoughts-2025-09-06\"\u003ePort implementation thoughts\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#nonissues-2025-09-06\"\u003eNon-issues\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#palettes-2025-09-06\"\u003ePalettized and planar graphics\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pages-2025-09-06\"\u003ePage flipping\u003c/a\u003e \u003c/li\u003e\u003c/ul\u003e\u003c/ol\u003e\u003chr id=\"strategy-2025-09-06\"\u003e\u003cp\u003e\n\tSo, how \u003ci\u003edo\u003c/i\u003e we get the PC-98 Touhou codebase into a portable state? That entirely depends on what kind of port we want in the first place, and how much of ZUN's code we are willing to change. Three particularly efficient options immediately come to mind:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tOn one end of the spectrum, we have a preconfigured PC-98 emulator with disabled configuration options and a stripped-down UI that tricks people into believing they're playing a port and prevents them from accidentally breaking the working configuration.\u003cbr\u003e\n\tThis might sound like a joke, but it's unironically the most efficient and pragmatic solution that will be good enough for the overwhelming majority of players. If you ask people what they expect from a port, they primarily name \u003cq\u003eease of use\u003c/q\u003e and \u003cq\u003enot having to configure emulators\u003c/q\u003e. Both of these can be solved with a preconfigured emulator and thus don't justify the monumental engineering effort of the more complex porting methods described below. That effort also wouldn't be justified if people just wanted \u003ci\u003ea\u003c/i\u003e port and had no standards regarding its technical implementation, besides maybe \u003cq\u003eno input lag\u003c/q\u003e. \u003ci\u003eSomeone\u003c/i\u003e has to put in the effort to solve every little challenge on the way from PC-98 to modern systems, and if that effort is not appreciated…\n\u003c/p\u003e\u003cp\u003e\n\tBy the way, I have no idea what people are talking about when they claim that PC-98 Touhou \u003cq\u003ehas input lag\u003c/q\u003e, because there sure is nothing like that in the code that would indicate anything above 1 frame\u0026nbsp;/ 17.7\u0026nbsp;ms for \u003cspan class=\"hovertext\" title=\"Menus are more complicated, but I don't think that's what people are typically referring to when they talk about input lag.\"\u003ethe in-game portions\u003c/span\u003e. Any investigation into these issues would therefore have to come from someone else, I'm afraid. Everything points to input lag being the result of misconfigured emulators.\n\u003c/p\u003e\u003cp\u003e\n\tThis is not like Shuusou Gyoku, where a port to modern APIs made sense because almost every subsystem still performs suboptimally on modern Windows even \u003ci\u003eafter\u003c/i\u003e you set up DxWnd, a better MIDI synth, and whatever people are using to make modern gamepads work with ancient DirectInput these days. If you correctly set up a PC-98 emulator, the games do run at full speed, and are highly likely to continue running fine after emulator and operating system version updates.\u003cbr\u003e\n\tThus, can we conclude that wishing for ports is primarily a symptom of the Touhou community's past failure and negligence to spread preconfigured emulators to people? Because this \u003ci\u003esurely\u003c/i\u003e shouldn't be a problem in this day and age anymore? While \u003ca href=\"https://github.com/nmlgc/np2debug/commit/a40ad5c19545323cb639faec771b0eae84f7334b\"\u003eI did my part way back in 2013\u003c/a\u003e, it would take until spaztron64's 2021 package for the community at large to finally wake up and realize that this was a problem. Nowadays though, we have at least three decent packages made by separate people that have my personal seal of approval. And yes, this even includes the offering you can obtain at a certain mountaintop place of worship. That site used to be infamous for pushing out slop that violated their own mission statement and externalized costs to the tech support departments of their supply chain, but I'm glad to announce that they've leveled up and now provide a decent solution. And once they remove that archive inside their archive, it will be even better!\u003cbr\u003e\n\tStill, if your emulator configuration guides are presented more prominently than your preconfigured emulator downloads, you're doing a disservice to the community. Make guides \u003ci\u003eavailable\u003c/i\u003e, yes, but clearly label them as background information for people who already played the games and \u003ci\u003ethen\u003c/i\u003e got curious about this old Japanese computer architecture.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tOK, but what if you do have standards and \u003ci\u003ewould\u003c/i\u003e appreciate a technically more solid port that removes layers and maybe even improves the games beyond the limits of the PC-98's architecture? If we take a single step towards native code and native performance, we end up with what people call a \"static recompilation\" these days. As I explained in \u003ca href=\"/faq#recomp\"\u003ethe FAQ entry I wrote last year\u003c/a\u003e, this kind of port would still emulate the graphics, sound, input, and memory subsystems of a PC-98, but it \u003ci\u003ewould\u003c/i\u003e cut out CPU emulation.\u003cbr\u003e\n\tFor PC-98 Touhou, this is actually quite a huge deal: CPU speed is the single biggest point of contention when configuring PC-98 emulators for Touhou, and the vastly different x86 cores of each emulator result in vastly different performance characteristics once you start to benchmark them all more thoroughly. With no more CPU cycles to count, we'd also lose all the VRAM access latencies that emulators typically strive to replicate, and thus pretty much guarantee 0% slowdown in the resulting port. While the aforementioned kind of modded emulator could theoretically also remove cycle counting and VRAM latencies, it would still interpret x86 instructions and thus have a harder time actually reaching the native performance required for 0% slowdown.\n\u003c/p\u003e\u003cp\u003e\n\tThis kind of port would also find immediate acceptance within the gameplay community. Since it would only take ZUN's original binaries as input and ignore our reconstructed source, we're guaranteed to retain the exact gameplay logic. The entire instruction translation process would be automated, leaving no room for modernizing the codebase by hand \u003ca href=\"/blog/2025-08-12#bug-2025-08-12\"\u003e📝 and accidentally breaking gameplay\u003c/a\u003e. We'd still have to defuse at least a few landmines to get the port running without issue, but those would be limited to things like \u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6d836b3a3142543830b519792f11938030863e4/th03/formats/cfg.hpp#L5\"\u003efilename casing\u003c/a\u003e, for example. Nothing even remotely close to gameplay code.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tOn the other end of the spectrum, we have something like \u003ca href=\"https://web.archive.org/web/20210411064709/https://m.newsmth.net/article/TouHou/single/11992\"\u003euth05win\u003c/a\u003e: A fully native rewrite of the graphics code that takes every liberty and cuts every corner it needs to rework the game into something that naturally renders within \u003cspan class=\"hovertext\" title=\"Or, in uth05win's case, the immediate-mode API of old OpenGL.\"\u003ea modern graphics API of our choice\u003c/span\u003e. Unlike uth05win, however, our ports will be based on complete decompilations and thus retain the original gameplay code instead of \u003ca href=\"https://github.com/KyoriAsh/uth05win/blob/9ad0c7a80948fd46408a538e515a8698e03b0871/Game/Stage/EnemyBullet/EnemyBullet.cpp#L636-L648\"\u003efreely rewriting certain parts because they look strange\u003c/a\u003e. In turn, we would basically scrap all of ZUN's menu and cutscene code and write quirk-free and sane replacements. Part 4 will drive home just how much more relaxing this course of action would have been…\u003cbr\u003e\n\tThere's certainly an argument to be had that a modern port \u003ci\u003eshould\u003c/i\u003e reimagine the game to look and feel as modern as you can get within the original assets, and \u003ci\u003enot\u003c/i\u003e stick to PC-98 limitations. After all, the unmodified PC-98 version is always there for you to play on your correctly configured emulator, right? In fact, if we ever wanted to port the games to weaker systems or consoles, this kind of port would be our only option.\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tBut as you might have guessed, we're not going for either of these options:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tThe first option doesn't even need anything from ReC98. Even the sleekest imaginable release could be done by anyone who either knows about PC-98 emulation or keeps in contact with someone who does, and is comfortable messing around with emulator source code. In fact, I'm not even a particularly qualified person for this job; I frequently mess with emulator configurations for research reasons, and then forget the correct values for certain obscure settings. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThis is such an obvious and efficient move that I seriously wonder why nobody has done it so far… but then again, I thought the same about every other idea I ended up doing myself in this space over the past 15 years. If that idea sounds great to you, feel free to go ahead – it represents the opposite of what this project is about, so the resulting fame is yours for the taking. If y'all see \"ports\" popping up from a place that isn't this project in the not-too-distant future, you can be pretty sure that their developers followed this strategy.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe second option would indeed be an interesting project in its own right, as I've stated in the FAQ entry. But if you remember \u003ca href=\"/blog/2024-07-09#msdos-2024-07-09\"\u003e📝 the last time I thought about static recompilation\u003c/a\u003e, I was way more excited for recompiling the old \u003ci\u003ecompiler\u003c/i\u003e we use for the PC-98 code rather than the games themselves. Ironically, this is primarily because of how much a recompilation would complicate the new features we plan to add to the games. Since I can only develop new features on top of a previous reverse-engineering effort, they will necessarily remain tied to the PC-98-native version of the codebase at first. How would we port them, then?\n\t\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eDo I continue developing these features for the PC-98 and then simply recompile them along with the rest of the game? The issue with that approach is that most features won't have a version that could work with the original ZUN codebase that we'd prefer to recompile. For everyone's sanity, most features will only exist as part of a respective game's \u003ccode\u003eanniversary\u003c/code\u003e branch, which in turn is based on the rearchitected and de-landmined \u003ccode\u003edebloated\u003c/code\u003e branch. Recompiling these branches would undermine the entire selling point of delivering the pure, untainted ZUN code that would have probably convinced the gameplay community to invest in this strategy in the first place. It might be good enough for the rest of the community, but if I'm going to rearchitect the PC-98 codebase \u003ci\u003eanyway\u003c/i\u003e, would there even be a point in developing the required recompilation techniques on the side? Would this give us ports faster than following a more classical approach?\u003c/li\u003e\n\t\t\u003cli\u003eThen again, I could still try slicing out the code for these features in a way that would allow them to be shared between the rearchitected PC-98 and recompiled ZUN codebases. But that's bound to create an unnatural and awkward mess that's probably even worse than the way I have to arrange ZUN's code on the unmodified \u003ccode\u003emaster\u003c/code\u003e branch. I'd definitely charge extra for that.\u003c/li\u003e\n\t\t\u003cli\u003eDo I just copy-paste and maintain two versions of the feature code for both platforms, manually transferring all required reverse-engineering to the recompilation? That might feel very dull, but it's probably more efficient than any attempt at sharing that code.\u003c/li\u003e\n\t\t\u003cli\u003eOr do I just abandon the PC-98-native codebase? In favor of a pseudo-PC-98 codebase that still very much \u003ci\u003eassumes\u003c/i\u003e PC-98 hardware but doesn't actually \u003ci\u003erun\u003c/i\u003e on real \u003ci\u003eor\u003c/i\u003e conventionally emulated PC-98 hardware… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003c/ul\u003e\u003cp\u003e\n\tThe last point in particular demonstrates just how little of a help a recompilation would actually be. Since it would continue to emulate the PC-98's graphics system, I'd still have to write any new graphics code against the PC-98's planar and two-page VRAM. Automatically porting the games to a friendlier and more generic rendering paradigm is infeasible for even an advanced recompiler: Every part of the original game expects PC-98 hardware, and a generic rewrite requires engineering decisions at a much higher level than the individual x86 instructions a recompilation operates at.\u003cbr\u003e\n\tAnd ultimately, it's these individual features that people should be (and mostly are) hyped for. Community-usable replays, translations, and TH03 netplay can all be implemented natively on PC-98. Sure, netplay would be easier to develop \u003ci\u003eand\u003c/i\u003e easier to use within a TH03 recompilation since we can just use the native network stack of your host OS \u003ca href=\"/blog/2024-04-24#compipes-2024-04-24\"\u003e📝 without any intermediaries\u003c/a\u003e. But developing both a recompiler \u003ci\u003eand\u003c/i\u003e netplay would still take longer than \u003ca href=\"/blog/2024-04-24#integration-2024-04-24\"\u003e📝 following through with our current PC-98-native plan\u003c/a\u003e.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe third option is actually quite popular, or would at least be acceptable to the majority of the general fandom. This is what non-technical people have in mind anyway when they think about ports, even if they don't confuse ports with remakes.\u003cbr\u003e\n\tTo find out just how acceptable such a port would be, I picked screen fade effects as a representative detail for the corners that such a port would cut, and \u003ca href=\"https://x.com/ReC98Project/status/1941624615316410492\"\u003easked how people judge the natural alpha-blended implementation in uth05win against the palette-based method you'd use on a PC-98\u003c/a\u003e. Surprisingly, a whopping 79% of respondents don't have any problem with a port using whatever is most natural for the system it runs on. And that's 79% of \u003ci\u003emy\u003c/i\u003e audience, which certainly is at least somewhat aware of PC-98 hardware details and the limitations that shaped these games into what they are. Of course, the 21% of die-hard PC-98 supremacists would then loudly complain that such a choice would make the port literally unplayable, but we could easily dismiss them by pointing to the poll where the community decided in favor of the smoother option. After all, ZUN's intention was to have \u003ci\u003ea\u003c/i\u003e fade, and manipulation of a 12-bit color palette was simply the only tool he had on a PC-98.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, the gameplay community has much higher hopes for ReC98. Both them and I don't just want to \u003ci\u003esupplement\u003c/i\u003e the original PC-98 versions with something that's playable on modern systems, but\n\t\u003c/p\u003e\u003cblockquote\u003e\u0026gt; replace the need for the proprietary, PC-98-exclusive original releases and their emulation for even the most conservative fan\u003c/blockquote\u003e\u003cp\u003e\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/e0ecdf40f29c724e9e4b93e256512f413d013d06\"\u003eas I wrote back in 2014\u003c/a\u003e. Sure, the community can manage spreading pre-configured emulators for a few more years, but wouldn't it be great if they \u003ci\u003ecould\u003c/i\u003e stop doing that at some point in the far future?\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tSo if all the \"easy\" solutions either don't have much of a purpose or disappoint in some way, we're only left with the hard one: A classic, manual port done primarily for the sake of solving an engineering challenge. But hey, this means that it'll also produce tons of blog posts for all of you to read, which apparently is at least equally as popular as actually \u003ci\u003eplaying\u003c/i\u003e the games. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tHere's what we're going to do:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eRearchitect the game to end up with one shared codebase that compiles for both PC-98 and modern systems, avoiding the code duplication drawback of static recompilation approaches.\u003c/li\u003e\n\t\u003cli\u003eAccept nothing less than a pixel-perfect port. The PC-98 and modern versions should look identical on every frame. It is not ReC98's job to reimagine the games; as usual, I'm going to do the hard work, and it's up to other modders to throw it all out and simplify it later.\u003c/li\u003e\n\t\u003cli\u003ePerform all the automated gameplay validation we possibly can to earn the trust of the gameplay community, avoiding debacles like \u003ca href=\"/blog/2025-05-20\"\u003e📝 the\u003c/a\u003e \u003ca href=\"/blog/2025-08-12\"\u003e📝 two\u003c/a\u003e recent desyncs in my Shuusou Gyoku build. This forces us to have a lightweight method of recording replays on top of the unmodified \u003ccode\u003emaster\u003c/code\u003e branch \u003ci\u003ebefore\u003c/i\u003e we can start porting – a fact that Ember2528 already somewhat identified within his current roadmap of funding priorities for TH03.\u003c/li\u003e\n\t\u003cli\u003eContinue fixing \u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#labeling-weird-or-broken-code\"\u003elandmines, bugs, and bloat\u003c/a\u003e. Many landmines must necessarily be fixed for a port to work at all, bugfixes are highly requested by most fans and backers, and bloat fixes ensure maintainability, moddability, and bring the PC-98 versions closer to the performance a modern port will naturally run at.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSure, the main drawback here is the immense development effort required. But in exchange, the port retains \u003ca href=\"/faq#automate\"\u003ereadable and moddable code\u003c/a\u003e and continues to deliver the insights that this project has always stood for. Imagine stepping through gameplay code using a native C/C++ debugger at your native screen resolution!\n\u003c/p\u003e\u003chr id=\"slowdown-2025-09-06\"\u003e\u003cp\u003e\n\tBut before we can get to \u003ci\u003ehow\u003c/i\u003e I'm going to do all that, there are two popular misconceptions I have to address.\n\u003c/p\u003e\u003ch3\u003e\u003cq\u003eAccurate slowdown\u003c/q\u003e\u003c/h3\u003e\u003cp\u003e\n\tThe initial version of \u003ca href=\"https://github.com/MaribelHearn/maribelhearn.com/commit/525ad47f04413bf31523e36d8b2989905e18c332\"\u003eMaribel Hearn's new emulator guide for PC-98 Touhou\u003c/a\u003e had the following sentence that spaztron64 and I successfully lobbied against:\n\u003c/p\u003e\u003cblockquote\u003eNote that none of the emulators have accurate slowdown; the slowdown will not match real hardware.\u003c/blockquote\u003e\u003cp\u003e\n\tObjectively, this is a true statement. Neko Project's i386 core is the closest thing to cycle-accurate PC-98 emulation we have, as its per-instruction cycle counts match Intel's documentation. But even \u003ci\u003eits\u003c/i\u003e performance characteristics are wildly inaccurate compared to a real PC-98 system with a 386, as we're going to see in the next blog post.\u003cbr\u003e\n\tThe problem I have with this sentence is that it's very misleading in this specific context. The mere mention of \u003cq\u003eaccurate slowdown\u003c/q\u003e in a beginner's guide on PC-98 emulation paints said slowdown as something desirable and worthy of preservation. It evokes stories of console speedrunners and emulator developers who deal with fixed, well-defined hardware where the concept of accurate slowdown makes sense. Stories that probably originated from a time before decompilations of classic games became commonplace, when it was hard to say whether a particular instance of slowdown was intended or not. And even with a decompilation, these things remain a matter of interpretation if you can't ask the original developer. Thus, it's completely understandable why observable behavior of real hardware remains the one benchmark of accuracy and quality that people can understand and rally around.\u003cbr\u003e\n\tThe PC-98, however, is very much not that kind of fixed system, but \u003ca href=\"https://en.wikipedia.org/wiki/PC-98#Models\"\u003ea computer architecture that spanned 18 years of hardware evolution\u003c/a\u003e, from 1982 to 2000. Even if we reduce this list of models to the ones that match ZUN's stated minimum system requirements, we're still looking at 7 years of hardware, running different microarchitectures at different clock speeds and with different resulting bottlenecks. If there's such a big variety of systems, which particular slowdown behavior should the ports even preserve?\n\u003c/p\u003e\u003cp\u003e\n\tThe obvious answer is \u003ci\u003e\"the one from the exact system ZUN wrote these games on\"\u003c/i\u003e, but we don't know that system. \u003ca href=\"/blog/2024-11-22#perf-2024-11-22\"\u003e📝 Last year\u003c/a\u003e, I claimed that ZUN developed these games on a PC-9821Xa7, but I didn't add a citation back then and can't find one now. The closest piece of related known info is \u003ca href=\"http://www.kt.rim.or.jp/~aotaka/am/get.htm\"\u003ethis note on the Amusement Makers page that hosts the official downloads for the trial versions\u003c/a\u003e, listing three PC-98 models that they confirmed to run the games without issues:\n\u003c/p\u003e\u003cblockquote lang=\"ja\" style=\"font-family: monospace; white-space: pre-wrap;\"\u003eなお当サークルでは\n    ・ NEC PC-9821Xs        i486DX2 66MHz\n    ・ NEC PC-9821La13      Pentium Processor (P54C) 133MHz\n    ・ EPSON PC-486MS       AMD 5x86-P133 換装\nなどで正常に動くことを確認しています\u003c/blockquote\u003e\u003cp\u003e\n\tThese models are one whole CPU generation apart and their clock speed differs by 100%. Which one of these is supposed to have the \u003cq\u003eaccurate slowdown\u003c/q\u003e?\u003cbr\u003e\n\tBut even if we knew, \u003ci\u003eit doesn't matter\u003c/i\u003e. The README is clear about ZUN's intentions:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher class=\"plaintext\"\u003e\n\t\u003cblockquote\n\tlang=\"ja\" class=\"active\" data-title=\"TH02, \u003ccode\u003e封魔録.TXT\u003c/code\u003e\"\n\u003e　　ＰＣ９８、またはその互換機専用です。（ＥＧＣ搭載機種）\n　　３８６以上で動作しますが、４８６ぐらい無いときついかも知れません。実際は\n　　ＶＲＡＭアクセスが速いことが重要です。\n　　オプションで、処理の重い演出を減らすことも出来ます。\n　　また、ＭＳＤＯＳが必要です\n\n　　ＣＰＵは４８６（６６ＭＨｚ）でしか動作確認を取っておりませんので、あんま\n　　り遅い機種ですと不幸かもしれません。\n　　ちなみに、４８６（６６ＭＨｚ）ですといっさい処理落ちや、欠けなどは出ませ\n　　ん。\u003c/blockquote\u003e\u003cblockquote\n\tlang=\"ja\" data-title=\"TH03, \u003ccode\u003e夢時空.TXT\u003c/code\u003e\"\n\u003e　　ＰＣ９８、またはその互換機専用です。（ＥＧＣ搭載機種）\n　　ＣＰＵ：４８６（６６ＭＨｚ）以上推奨\n　　　　　（３８６でも動作はしますがゲームにならないでしょう。\n　　　　　　ただし、３８６命令を使っているので２８６は不可です。\n　　　　　　また、低クロックの４８６でもかなり処理落ちするかも知れません）\n\n　　実際は、ＣＰＵの他にもＶＲＡＭアクセスが速いことも重要です。\u003c/blockquote\u003e\u003cblockquote\n\tlang=\"ja\" data-title=\"TH04, \u003ccode\u003e夢時空.TXT\u003c/code\u003e\"\n\u003e　　ＰＣ９８（ＰＣ９８－ＮＸ除く）、またはその互換機専用です。\n　　（ＥＧＣ搭載機種）\n　　ＣＰＵ：４８６（６６ＭＨｚ）以上推奨\n　　　　　（３８６でも動作はしますがゲームにならないでしょう。\n　　　　　　ただし、３８６命令を使っているので２８６は不可です。\n　　　　　　はっきり言って、４８６でも６６ＭＨｚ位はないとかなり\n　　　　　　処理落ちするかも知れません）\n\n　　実際は、ＣＰＵの他にもＶＲＡＭアクセスが速いことも重要です。\u003c/blockquote\u003e\u003cblockquote\n\tlang=\"ja\" data-title=\"TH05, \u003ccode\u003e怪綺談.TXT\u003c/code\u003e\"\n\u003e　　●ＰＣ９８（ＰＣ９８－ＮＸ除く）、またはその互換機専用です。\n　　　（ＥＧＣ搭載機種）\n　　●ＣＰＵ：４８６（６６ＭＨｚ）以上推奨\n　　　　　（３８６でも動作はしますがゲームにならないでしょう。\n　　　　　　ただし、３８６命令を使っているので２８６は不可です。\n　　　　　　はっきり言って、４８６でも６６ＭＨｚ位はないとかなり\n　　　　　　処理落ちするかも知れません）\n　　　　　　実際は、ＣＰＵの他にもＶＲＡＭアクセスが速いことも重要です。\u003c/blockquote\u003e\n\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003c/figure\u003e\u003cp\u003e\n\tIf ZUN recommends \u003cq\u003ea 486 or faster to avoid slowdown\u003c/q\u003e, this necessarily means that any unintentional slowdown is indeed unwanted.\u003cbr\u003e\n\tAlso, note how only TH02's README claims that the game was exclusively tested on a 66\u0026nbsp;MHz model, which is highly likely to be that PC-9821Xs listed on the Amusement Makers page. Did ZUN switch to a faster PC-98 model for the development of the last three games? That late into the architecture's lifespan? Or did he merely \u003ci\u003etest\u003c/i\u003e the game on faster models while the main development still took place on his 66\u0026nbsp;MHz model?\n\u003c/p\u003e\u003ch4 id=\"config-2025-09-06\"\u003ePicking a CPU clock speed for emulators\u003c/h4\u003e\u003cp\u003e\n\tOf course, this now creates a problem for everyone wanting to configure emulators for PC-98 Touhou. If the ideal Touhou machine is infinitely fast, we should always pick the fastest possible emulated CPU speed, right? Historically, this has been bad advice: Most emulators will then stick to \u003ci\u003eexactly\u003c/i\u003e the amount of cycles per emulated second you specified in the menu, slowing down the emulated system as a result. It's this kind of emulator behavior that gets players to manually look for \u003ci\u003e\"the sweet spot\"\u003c/i\u003e – the maximum possible explicitly specified CPU clock speed that still manages to render without slowdown on their system. This is a tragedy for many reasons:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eRegular players probably don't analyze performance with any kind of rigor. I certainly have never heard them say how they made sure to record a video at 56.423\u0026nbsp;FPS and then stepped through its individual frames to confirm the absence of lag.\u003c/li\u003e\n\t\u003cli\u003eInstead, they will probably present their clock speed configuration as a general recommendation to others, without realizing that the \"sweet spot\" they found is specific to \u003ci\u003etheir\u003c/i\u003e system. If others then try this clock speed on a slower CPU, they get slowdown instead, and thus gain an entirely wrong impression about how fast the game is supposed to run, backed by a presumptive expert on the topic.\u003cbr\u003e\n\tAdmittedly, this will become less likely as time marches on, CPUs get faster, and emulators keep optimizing their x86 cores. \u003c/li\u003e\n\t\u003cli\u003eBut really, why are we expecting players to do this?!\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tEver since 2019, however, \u003ca href=\"https://twitter.com/simk98l\"\u003eSimK\u003c/a\u003e has been developing an \u003ci\u003eAsync CPU\u003c/i\u003e mode for Neko Project 21/W, which\n\t\u003ca href=\"https://simk98.github.io/np21w/version.html\"\u003efinally got stabilized in ver0.86 rev.93, back in April of this year\u003c/a\u003e. Activate this mode with the \u003ci\u003eScreen → CPU clock stabilizer\u003c/i\u003e and \u003ci\u003eScreen → Dynamic CPU clock adjustment\u003c/i\u003e options, and then you should theoretically be able to finally stop worrying: Just specify the maximum possible clock speed in the usual configuration menu, and Neko Project will dynamically reduce the emulated clock speed to the fastest speed your system can handle.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 644px;\"\u003e\u003cimg\n\tsrc=\"/blog/static/2025-09-06-NP21W-Async-CPU-settings.webp?65c1b056\" width=\"644\" alt=\"Screenshot of Neko Project 21/W's Async CPU options\"\n\u003e\u003c/figure\u003e\u003cp\u003e\n\tThen, the games are supposed to run similarly to how a correctly configured Anex86 has been running them all along, but with an additional 21 years of emulation accuracy improvements.\u003cbr\u003e\n\tSadly, this mode still needs a bit of work. Excessively high clock speeds will result in wildly fluctuating frame rates and even BGM tempos during the first few seconds of a game session as Neko Project 21/W apparently takes a while to find the optimal clock speed. Even afterwards, emulation remains noticeably slower than Anex86:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.webp?4f11c998\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423\" data-frame-count=\"571\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.avi?855a18e0\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.webm?f73ec9ec\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.webm?048d42dd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.webm?26015ac4\" type=\"video/webm\"\u003eVideo of TH04/TH05's ZUN Soft logo animation, recorded in Neko Project 21/W ver.0.86 rev.95 configured with a clock speed of 2.4576 × 407 = 1000.2432 MHz, running on an Intel Core i5-8400T, demonstrating a wildly fluctuating frame rate and BGM tempo. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH04-ZUN-Soft-1-GHz-fluctuating.avi?855a18e0\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThis is Neko Project 21/W ver.0.86 rev.95 configured with a clock speed of 1\u0026nbsp;GHz, running on an Intel Core i5-8400T. The fluctuations are not nearly as intense during the rest of a game session, but remain noticeable throughout.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut what about DOSBox-X, the other good emulator recommended these days? This Async CPU mode is very similar to the \u003ccode\u003ecycles=max\u003c/code\u003e option that DOSBox-X has supported all along. If you try running my \u003ca href=\"/blog/2023-03-05#blitperf-2023-2023-03-05\"\u003e📝 past\u003c/a\u003e and future blitting benchmarks using this option, you can observe how DOSBox-X also starts with a low cycle count and then gradually speeds up to accommodate the actual processing load.\u003cbr\u003e\n\tIn the much less synthetic test case of running PC-98 Touhou, however, DOSBox-X's cycle adjustment reveals itself as much more sophisticated than Neko Project 21/W's implementation. The \u003ccode\u003eshowdetails=true\u003c/code\u003e option reveals that the cycle count does fluctuate quite heavily, which does translate into minor BGM dropouts particularly near the start of a session. But these dropouts are tiny in comparison to what you'd get on Neko Project 21/W, and the framerate remains stable throughout.\n\u003c/p\u003e\u003cp\u003e\n\tAs for overall performance, DOSBox-X's \u003ccode\u003esimple\u003c/code\u003e interpreter core is not nearly as optimized as Neko Project 21/W's interpreter and peaks at roughly half of its speed. The \u003ccode\u003edynamic_nodhfpu\u003c/code\u003e core, however, solidly beats Neko Project 21/W by the same 50%. And it's this added bit of performance that makes all the difference: It eradicates slowdown in most of the usual spots in PC-98 Touhou where emulators and even Anex86 typically struggle, and turns DOSBox-X into the first emulator to finally beat Anex86's performance on the same hardware in all the workloads that matter. The dynamic core still doesn't \u003ci\u003equite\u003c/i\u003e reach the speeds of the hypothetical infinitely fast PC-98 on my outdated system, but it remains the most reliable configuration option when it comes to delivering ZUN's intended vision. If we ignore the BGM dropouts. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tJust make sure to explicitly select the  \u003ccode\u003edynamic_nodhfpu\u003c/code\u003e variant, \u003ci\u003enot\u003c/i\u003e the regular \u003ccode\u003edynamic\u003c/code\u003e core. The latter is infamous for \u003ca href=\"https://github.com/joncampbell123/dosbox-x/issues/2395\"\u003erecompilation errors in FPU code that break TH01 gameplay\u003c/a\u003e. While that specific issue is ostensibly fixed, I still managed to occasionally run into smaller FPU-related bugs in current DOSBox-X versions. Unfortunately, I didn't manage to capture them on video; I would have reopened the issue on the spot if I did.\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-blitperf-wide-DOSBox-X-simple-max.webp?bd59f269\"\n\t\tdata-title=\"DOSBox-X, max cycles, simple core\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the DOSBox-X with `cycles=max` and `core=simple`\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-blitperf-wide-NP21W-async-1-GHz.webp?596823ae\"\n\t\tdata-title=\"Neko Project 21/W, Async @1\u0026nbsp;GHz\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the Neko Project 21/W in Async CPU mode with a maximum clock speed of 2.4576\u0026nbsp;× 407\u0026nbsp;= 1000.2432\u0026nbsp;MHz\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-blitperf-wide-DOSBox-X-dynamic-max.webp?76a2018c\"\n\t\tdata-title=\"DOSBox-X, max cycles, dynamic core\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the DOSBox-X with `cycles=max` and `core=dynamic`\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-blitperf-wide-Anex86-sync-1.webp?bfd95ca7\"\n\t\tdata-title=\"Anex86, Sync 1\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the Anex86 in Sync 1 mode\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tOf course, any performance measurement of an emulator with dynamic cycle adjustment can only ever represent a snapshot of the ever-changing adjustment state, and should therefore be taken with a grain of salt. Hence, these screenshots are purely decorative; I just added them because I'm sure that \u003ci\u003esomeone\u003c/i\u003e would have asked for exact numbers otherwise. Also, the exact relations between emulators are \u003ci\u003ehighly\u003c/i\u003e dependent on the workload…\u003cbr\u003e\n\t\tAnd yes, that's a new benchmark! More about this one \u003ca href=\"/blog/2025-09-10#wide-2025-09-10\"\u003e📝 in part 2\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t(Still, it's remarkable how close Anex86 gets despite its interpreter core, and how it even beats DOSBox-X in MOVS performance. I looked at Anex86's disassembly for 10 minutes and saw big tables of tiny per-instruction functions with custom calling conventions that make remarkably efficient use of the few registers you get in x86. Also, negative offsets? They must have written this entire x86-on-x86 core in ASM.)\n\u003c/p\u003e\u003cp id=\"66-2025-09-06\"\u003e\n\tWhile this is great news for players, the whole situation remains very unsatisfying at a technical level. Even if you don't care about the remaining BGM dropouts, running these games at the highest possible emulated clock speed means that you constantly spend 100% of all CPU cores assigned to your emulator just to avoid slowdown and lag in a few particularly CPU-intensive sections. Power saving might be the single best practical argument in favor of a port. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAlso, all this complexity involved in dynamic cycle adjustment raises one question you might have had all along. Why don't we just leave our emulated CPUs at 66\u0026nbsp;MHz? After all, ZUN said that 66\u0026nbsp;MHz is enough to eliminate all slowdown in at least TH02 and TH03, so how about just living with whatever slowdown we'd still experience in TH04 and TH05? This is certainly a healthier approach, much more appropriate for these silly little indie games that were never meant to be obsessed about at this level, \u003ci\u003eand\u003c/i\u003e we get rid of those last few BGM dropouts in DOSBox-X!\u003cbr\u003e\n\tWell, if that statement was ever correct to begin with, it would have only applied to real hardware and not to emulators. \u003ca href=\"https://www.twitch.tv/Mu021\"\u003emu021\u003c/a\u003e reported that the final phase of TH02's Mima fight slowed down even at 78\u0026nbsp;MHz in Neko Project, and part 4 will contain \u003ca href=\"/blog/2025-09-29#retain-2025-09-29\"\u003e📝 even more examples of how 66\u0026nbsp;MHz slows down several effects in menus and cutscenes\u003c/a\u003e, and thus paints a wrong picture of them. Hence, choosing 66\u0026nbsp;MHz for a preconfigured emulator package \u003ci\u003emight\u003c/i\u003e have a particularly annoying side effect: If people get used to how slow these effects run on emulators, they might be rather irritated once the modern ports will invariably run them at their intended speed denoted in the code. I can already imagine them yelling \u003ci\u003e\u003cq\u003etoo fast!\u003c/q\u003e\u003c/i\u003e, \u003ci\u003e\u003cq\u003einaccurate!\u003c/q\u003e\u003c/i\u003e, and \u003ci\u003e\u003cq\u003eliterally unplayable!\u003c/q\u003e\u003c/i\u003e, oblivious to the fact that they had the wrong idea about these effects all along.\u003cbr\u003e\n\tOr maybe it'll all be fine once part 4 has documented these issues in depth. I certainly wouldn't criticize a package for choosing 66\u0026nbsp;MHz. All choices are unsatisfying at some level…\n\u003c/p\u003e\u003cp\u003e\n\tIf only we could optimize the games enough to remove any unwanted slowdown at 66\u0026nbsp;MHz. Then, people could freely choose one emulator over another for reasons unrelated to performance, because even cycle-limited emulators could then actually deliver on ZUN's statements in the README files… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAnd since we've defined debloating as an integral part of port development earlier, that's exactly what we're going to do.\n\u003c/p\u003e\u003chr id=\"fp-2025-09-06\"\u003e\u003cp\u003e\n\tBut \u003ci\u003ecan\u003c/i\u003e we even do that within our high standards? Obviously, our ports should remain…\n\u003c/p\u003e\u003ch3\u003e\u003cq\u003eFrame-perfect\u003c/q\u003e\u003c/h3\u003e\u003cp\u003e\n\tSince all five games are explicitly timed around VSync, it's immediately clear what we mean by this term:\n\u003c/p\u003e\u003cblockquote\u003eEverything rendered to a single page of VRAM between two VSync wait loops defines one single logical frame.\u003c/blockquote\u003e\u003cp\u003e\n\tIf we are double-buffering correctly and the PC-98 system running the game is fast enough to finish rendering such a logical frame to VRAM within two VSync signals, everything is fine: The sequence of frames you can observe on your screen matches the logical sequence of internal frames, and we can easily record this sequence and compare the port against it.\u003cbr\u003e\n\tBut what about unintentional slowdown? In these cases, ZUN asks the system to do way more work than it can execute between two VSync signals. Notably, this also includes most loading times: Once we add disk access into the mix, we can't guarantee hitting \u003ci\u003eany\u003c/i\u003e VSync deadlines anymore, and decompressing all these 640×400 images is quite expensive as well. Obviously, we don't want to abandon our goal of frame-perfection and the comparability of ports just because of this variability, so let's add another rule:\n\u003c/p\u003e\u003cblockquote\u003eIndividual defined frames may be shown on screen for any integer multiple of the frame time.\u003c/blockquote\u003e\u003cp\u003e\n\tThe reason for the integer restriction is obvious: If we start drawing to the screen in the middle of a frame, we get screen tearing and thus a non-perfect frame – not just because tearing looks bad, but also because the position of the tearing line always depends on the overall performance of the system you run the game on.\u003cbr\u003e\n\tThe combination of these two rules leads to an immediate consequence:\n\u003c/p\u003e\u003cp\u003e\u003cblockquote\u003eThe games must only ever display complete logical frames.\u003c/blockquote\u003e\u003cp\u003e\n\tAnd now we have a problem. Our rules have just outlawed screen tearing, but \u003ci\u003enearly every menu and cutscene screen in ZUN's original code has some kind of screen tearing issue\u003c/i\u003e. \u003ca href=\"/blog/2024-02-03#mess-2024-02-03\"\u003e📝 The Music Room of TH02-TH04\u003c/a\u003e represents probably the worst example as it suffers from screen tearing on every single frame:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated bglayer\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-TH04-Music-Room-tearing.webp?247da0c5\"\n\t\tdata-title=\"Raw image\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH04's Music Room, demonstrating the screen tearing landmine that the original game exhibits on every frame\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-09-06-TH04-Music-Room-tearing-visualization.webp?c400b23a\"\n\t\tdata-title=\"Tearing visualization\"\n\t\twidth=\"640\"\n\t\talt=\"Colored visualization of which lines correspond to which frame in the TH04 Music Room screenshot\"\n\t\tstyle=\"background-image: url('/blog/static/2025-09-06-TH04-Music-Room-tearing.webp?247da0c5');\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tWhich frame are we even on? 😵 This landmine is the reason why the first rule explicitly restricts rendering to a single VRAM page. \u003ca href=\"/blog/2024-02-03#mess-2024-02-03\"\u003e📝 Check the Music Room blog post for an explanation of what went wrong here.\u003c/a\u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAlso, how would you possibly preserve these tearing lines once you've ported the game? After all, modern platforms not only imply much faster CPUs, but also completely different rendering methods, especially once we add scaling into the mix.\u003cbr\u003e\n\tThis can only mean one thing:\n\u003c/p\u003e\u003cp\u003e\n\t\u003cstrong\u003eIt is fundamentally impossible to port the unmodified codebase of PC-98 Touhou and remain \u003cq\u003eframe-perfect\u003c/q\u003e to the original release.\u003c/strong\u003e\n\u003c/p\u003e\u003cp\u003e\n\tYou could maybe get there by throwing out the integer multiple rule and accepting teared frames as legitimate. But then you'd have to decide on a particular model whose slowdown behavior you'd want to replicate and lock down exactly – and as I've stated in the section above, that's quite a silly and impractical proposition.\u003cbr\u003e\n\u003c/p\u003e\u003ch4 id=\"solve-2025-09-06\"\u003eResolving screen tearing\u003c/h4\u003e\u003cp\u003e\n\tSo, how do we get back to a comparable sequence of well-defined frames? This can only work if we leave the confines of real hardware and instead reach for the infinitely fast PC-98 that ZUN wanted to have anyway. Such a system would never exhibit screen tearing because it would naturally complete all rendering within the \u003ca href=\"https://en.wikipedia.org/wiki/Vertical_blanking_interval\"\u003evertical blanking interval\u003c/a\u003e preceding each displayed frame. Once our code then ends a frame by entering a busy-waiting loop for the next VSync signal, the screen would then get to draw static and well-defined VRAM contents. This behavior is the whole reason why I get to classify screen tearing issues as \u003ci\u003elandmines\u003c/i\u003e that must always be fixed, as opposed to \u003ci\u003ebugs\u003c/i\u003e that a port could potentially retain.\u003cbr\u003e\n\tIf we actually had such an infinitely fast PC-98, we could just run ZUN's unmodified code on that system and be done now. But as we've seen above, not even DOSBox-X's dynamic core manages to run PC-98 Touhou at the infinitely fast level we'd need. Also, we wanted to get rid of relying on specific emulators and have already planned to optimize all this code anyway…\n\u003c/p\u003e\u003cp\u003e\n\tSo let's defuse each screen tearing landmine one by one by rewriting its code to match the output of an infinitely fast PC-98. This is a lot more feasible than it sounds because these landmines aren't actually caused by a lack of CPU power. Every screen tearing issue comes down to ZUN misplacing certain screen-affecting operations within the hellscape of imperative hardware state mutations that is his menu and cutscene code. You can either hide the issue by throwing an infinite amount of processing power at the problem so that the order of mutations no longer observably matters, or you can just write good code.\u003cbr\u003e\n\tIn theory, we only have to follow a few rules:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAll VRAM page flips and hardware palette changes must be moved to the vertical blanking interval.\u003c/li\u003e\n\t\u003cli\u003eSince \u003cspan class=\"hovertext\" title=\"Apparently, TRAM can be page-flipped just like VRAM, but I don't know any documentation or examples that show how it's done. What is that extra 4 KiB of TRAM between 0xA1000 and 0xA2000 even good for???\"\u003eTRAM is always single-buffered\u003c/span\u003e and ZUN rarely writes to the topmost rows, we can get by with merely moving TRAM writes close to the vertical blanking interval if we don't manage to hit the interval exactly.\u003c/li\u003e\n\t\u003cli\u003eOn single-buffered screens, the same is true for VRAM. This category mainly includes menu screens whose upper VRAM rows thankfully remain static, so we also get some leeway here. Rewriting these screens to be double-buffered might sound better, but doing so at the high level where these landmines have to be fixed would only create more of a mess, \u003ca href=\"/blog/2025-09-06#pages-2025-09-06\"\u003e📝 for reasons I'll explain below\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eIn rare cases, ZUN placed expensive file load calls \u003ci\u003eand\u003c/i\u003e draw calls on the same logical frame within a single-buffered screen. For an infinitely fast PC-98, this is no problem. But since all bets are off once disk access is involved, there is no way we can hide the draw calls and avoid the resulting screen tearing on real hardware and emulators while still sticking to ZUN's defined sequence of logical frames. Thus, we have to make an exception and insert an additional VSync delay loop after the load calls to separate loading and rendering, creating a new logical frame that did not exist in ZUN's original code.\u003cbr\u003e\n\tThis might sound very controversial. We've just come up with this mental model of an infinitely fast PC-98 to solve frame-perfection, only to now deviate from it again and snap back to reality? However:\u003cul\u003e\n\t\t\u003cli\u003eAs I'm going to describe in \u003ca href=\"/blog/2025-09-10#pi-2025-09-10\"\u003e📝 part 2\u003c/a\u003e, we're about to speed up loading and blitting by much more than this one added frame.\u003c/li\u003e\n\t\t\u003cli\u003eIf we run this logical frame on the actual fastest real-hardware PC-98 system the community has to offer and even \u003ci\u003ethat\u003c/i\u003e system takes longer than 17.7\u0026nbsp;ms to render it, it's hard to argue against formalizing a delay you'd be getting on real hardware anyway.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe difficulty of actually pulling this off, however, can range anywhere from Easy to Lunatic, depending on the screen, because \u003ci\u003eof course\u003c/i\u003e every one of them is different. Even after these 11 pushes, I'll be far from done. But in the end, we'll have perfect \u003ci\u003eand\u003c/i\u003e easily verifiable frame parity between the PC-98 versions and the future ports, even though we had to bend the code a little. Or a lot. Oh well.\n\u003c/p\u003e\u003chr id=\"thoughts-2025-09-06\"\u003e\u003cp\u003e\n\tIf you only opened this post for the required reading part, you can stop reading now. I've got a few more technical thoughts about a few implementation details of the future ports that tend to come up in discussions, but these aren't as essential as the high-level issues above.\n\u003c/p\u003e\u003cp id=\"nonissues-2025-09-06\"\u003e\n\tSo we've now decided on what to do in order to make the ports \u003ci\u003egood\u003c/i\u003e, but what are the basic challenges we have to solve in order to port these games to modern systems in the first place? Let's start with a perhaps surprising list of non-issues that some people might perceive as challenges:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSound. As people of culture, we can all agree that PCM recordings of sequenced sound are sacrilegious, so the ports will always use some kind of emulation here. Therefore, I'll simply ask sound people for the best YM2608 and PMD cores that won't get me canceled. If I still get canceled, we'll just resolve the disagreement with a violent flamew- I mean, a constructive discussion, or just offer multiple options if there are valid arguments for either choice – similar to how you can \u003ca href=\"/blog/2024-03-09#choice-2024-03-09\"\u003e📝 choose between real SC-88Pro or virtual Sound Canvas VA recordings for my Shuusou Gyoku build\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eTH03's SPRITE16-powered in-game renderer. For a port, it does not matter at all \u003ci\u003ehow\u003c/i\u003e a sprite driver was originally implemented. ZUN already streamlined regular sprite blitting down to \u003ca href=\"https://github.com/nmlgc/ReC98/blob/04724c9f2f88bb4755a6eb7f21b96c84f8e7ee1e/th03/main/sprite16.hpp#L66-L84\"\u003ethree common functions\u003c/a\u003e, which a port would simply need to implement differently. The game code still contains 21 additional calls to SPRITE16 functions for certain special effects, but none of \u003ca href=\"https://github.com/nmlgc/ReC98/blob/04724c9f2f88bb4755a6eb7f21b96c84f8e7ee1e/libs/sprite16/sprite16.h#L17-L32\"\u003ethese additional monochrome, masked, or overlapped blitting modes\u003c/a\u003e are unique to TH03.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn short: If the feature in question is consistently used through an API, it's not a challenge in itself. The hard parts are all the opposite cases – when ZUN suddenly starts writing to VRAM segments or I/O ports in the middle of gameplay code, like he does all over TH01. All of these instances need to be manually cleaned up and abstracted away. Conversely, this is also why \u003ca href=\"/blog/2023-03-30#mistakes-2023-03-30\"\u003e📝 TH02 remains by far the easiest individual game to port\u003c/a\u003e – it has the least amount of hand-written blitting code and mostly sticks to master.lib functions.\n\u003c/p\u003e\u003cp\u003e\n\tInstead, the biggest immediate challenge is something far more basic:\n\u003c/p\u003e\u003ch3 id=\"palettes-2025-09-06\"\u003e🎨 Palettized and planar graphics 🎨\u003c/h3\u003e\u003cp\u003e\n\tAfter all, PC-98 Touhou doesn't just view the PC-98's graphics subsystem as an obstacle to overcome, but occasionally makes creative use of both palettes and individual bitplanes. How would we possibly cover these effects in a modern graphics API that will be far removed from these concepts? Three challenges immediately come to mind in that regard:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\n\tThe whole concept of enforcing a single 16-color palette across the entire screen in a world where 32-bit RGBA is the only reliably available texture format. Shaders offer a simple solution: We simply wouldn't use traditional textures, and just write \u003ca href=\"https://www.khronos.org/opengl/wiki/Common_Mistakes#Paletted_textures\"\u003eour own sampler that takes both the original palettized 16-color+alpha image and the global palette as input, and performs a lookup for each texel\u003c/a\u003e. But what are we supposed to do in SDL_Renderer's fixed-function pipeline? Use the CPU to update all loaded textures on every palette color change? Split each sprite into a separate texture for each color and consume 16× the amount of VRAM just so that we can use vertex colors for each individual color layer? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Or break down every sprite into a point list to save the VRAM? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/li\u003e\u003cli\u003e\n\tAny kind of sprite-shaped palette color bit flipping effect, such as \u003ca href=\"/blog/2024-02-03#b-2024-02-03\"\u003e📝 the falling polygons in the Music Room\u003c/a\u003e. Effects like these \u003ci\u003ecould\u003c/i\u003e potentially be hardware-rendered even in a fixed-function pipeline if we split the background image into two and render the polygons using regular triangles with their UV coordinates matched to the pixel coordinates on every frame. But would all the involved interpolation reliably give us the original sharp edges without reaching for a shader to ensure that it does? In any case, this solution would need a completely different implementation for a modern port than it currently uses in ZUN's PC-98-native code, which gets by with less per-frame redraw than you'd think that this effect would need.\u003cbr\u003e\n\tuth05win didn't even get to port the Music Room, which is probably not without reason.\n\u003c/li\u003e\u003cli\u003e\n\tTH01's square-shaped inverting effects used during bomb and entrance animations. Flipping a given bit of a pixel's palette index? Based on what's there before? No way around a shader for this one…\n\t\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH01-Bomb.webp?0e4ddb1b\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"146\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH01-Bomb.avi?20fd5f2b\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH01-Bomb.webm?5a4b6c00\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH01-Bomb.webm?896b3f39\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH01-Bomb.webm?82b48ac2\" type=\"video/webm\"\u003eVideo of TH01's bomb animation, recorded in Makai Stage 12. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH01-Bomb.avi?20fd5f2b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003cfigcaption\u003e\n\t\tNote how the flipped cards rip holes into the square trails. I'm not even sure what the TH01 Anniversary Edition would change about the effect, or whether it even \u003ci\u003eshould\u003c/i\u003e change anything about it. Good luck porting this effect pixel-perfectly without pixel-level access.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\n\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tHowever, writing all this custom graphics code for the modern port would run against my previously stated goal of sharing as much code as possible between PC-98 and modern platforms. While shaders are the conceptually simpler solution for all of these challenges, they aren't \u003ci\u003eeasy\u003c/i\u003e in practical terms, and I already \u003ca href=\"/blog/2024-10-22#lens-2024-10-22\"\u003e📝 decided against using them for Shuusou Gyoku for good reasons\u003c/a\u003e. Also, is all of this really worth the effort if these games demonstrably don't even \u003ci\u003eneed\u003c/i\u003e the performance of GPU rendering?\u003cbr\u003e\n\tBut that only leaves one conclusion:\n\u003c/p\u003e\u003cp\u003e\n\t\u003cstrong\u003eThe future ports of PC-98 Touhou to modern systems will software-render the graphics layer on the CPU.\u003c/strong\u003e\n\u003c/p\u003e\u003cp\u003e\n\tI know, that sounds very shocking and probably disappointing at first. But at a closer look, it's really not all that bad. These games have been software-rendered all along by not only PC-98 emulators, but by real hardware at mid-90's CPU speeds. You might point to the GRCG and EGC chips as evidence for at least some capacity of hardware acceleration, but I see them more as workarounds for the unfortunate planar nature of VRAM on this Japanese business computer architecture. In the end, \u003ci\u003e\"software rendering\"\u003c/i\u003e only means that the CPU receives access to every pixel in the framebuffer. Once all graphical functionality is neatly abstracted away and the game no longer directly accesses the four physical bitplanes, the ports can store sprites and the rendered graphics layer in the most performant way.\u003cbr\u003e\n\tAlso, note how I only said \u003ci\u003e\"graphics layer\"\u003c/i\u003e. Besides \u003ca href=\"/blog/2024-10-22#lens-2024-10-22\"\u003e📝 the obvious candidate of framebuffer scaling\u003c/a\u003e, the ports will use the GPU for two more important aspects:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThe PC-98's text layer. With 8 fixed colors and glyphs drawn from a more or less static font ROM/\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e texture, there is no reason not to render this layer entirely on the GPU. Even \u003cspan style=\"background-color: var(--c-text); color: var(--c-bg)\"\u003ecolor reversing\u003c/span\u003e is as simple as defining a custom blend mode that inverts the alpha channel, \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_ComposeCustomBlendMode\"\u003ewhich SDL supports for all of its renderer backends\u003c/a\u003e.\n\u003c/li\u003e\u003cli\u003e\n\tVertical scrolling. \u003ca href=\"/blog/2023-06-30\"\u003e📝 The original games also reach for a PC-98 hardware feature here\u003c/a\u003e, and this feature can be replicated within 3D APIs in exactly the same way by adjusting the UV coordinates of the VRAM texture. This insight reduces the software renderer's required per-frame redraw to exactly the same amount as the PC-98 version, and should defeat any remaining concerns you might have about software rendering.\u003cbr\u003e\n\tThe still image in that post from two years ago doesn't demonstrate the PC-98 way of VRAM scrolling all too well, so here's a longer video that scrolls an entire screen's worth of tiles:\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH05-DEMO5.REC-unscrolled.webp?92dd9e0f\" preload=\"none\" controls data-title=\"Unscrolled VRAM\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"1600\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH05-DEMO5.REC-unscrolled.avi?6ef3daf9\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH05-DEMO5.REC-unscrolled.webm?917f3a51\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH05-DEMO5.REC-unscrolled.webm?e5c3b65e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH05-DEMO5.REC-unscrolled.webm?3bf0d329\" type=\"video/webm\"\u003eVideo of a single screen from TH05's Extra Stage, as it appears in the original game. Taken from ZUN's hidden TH05 Extra Stage replay with Mima. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH05-DEMO5.REC-unscrolled.avi?6ef3daf9\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"606\" data-title=\"Flickering redraw\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1534\" data-title=\"Row 0 on top\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH05-DEMO5.REC-scrolling.webp?0006bc75\" preload=\"none\" controls data-title=\"Scrolling\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"1600\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH05-DEMO5.REC-scrolling.avi?1d05b464\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH05-DEMO5.REC-scrolling.webm?7f6ce9de\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH05-DEMO5.REC-scrolling.webm?36f29fd2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH05-DEMO5.REC-scrolling.webm?67e1553e\" type=\"video/webm\"\u003eVideo of a single screen from TH05's Extra Stage, as it appears in the original game. Taken from ZUN's hidden TH05 Extra Stage replay with Mima. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH05-DEMO5.REC-scrolling.avi?1d05b464\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"1534\" data-title=\"Row 0 on top\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003e\n\t\tIn the game logic, all entity positions represent the scrolled on-screen view, while the sprites are offset by the Y coordinate of the \u003cspan style=\"color: #20AA20\"\u003egreen\u003c/span\u003e line (representing the top of the scrolled screen) before they are blitted. Also note how ZUN never redraws the area between the yellow line (representing the bottom of the playfield) and the green line as part of the scrolling process, since it's always covered by a 16-pixel row of black TRAM cells. Any redraws there are a result of regular tile invalidation caused by overlapping sprites, and remain isolated to the VRAM page that the game rendered to when the overlap happened.\u003cbr\u003e\n\t\tThe gameplay is taken from \u003ca href=\"/blog/2020-09-21\"\u003e📝 ZUN's hidden TH05 Extra Stage Clear replay\u003c/a\u003e.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tAs a result, the software renderer of our hand-crafted ports would still internally produce a graphics and text layer that persists across frames and receives minimal redraws, just like the PC-98 originals did. In fact, it would have to produce the exact \u003ci\u003esame\u003c/i\u003e graphics layer if we wanted to port the non-Anniversary Edition, including the tile source area. There's no technical need to keep tiles on the graphics layer in a port, but certain intense shake effects temporarily reveal individual tiles below the HUD:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH02-Stage-3-shake.webp?4858046d\" preload=\"none\" controls data-title=\"Original TRAM contents\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"381\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH02-Stage-3-shake.avi?93b9beb7\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH02-Stage-3-shake.webm?75701ffa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH02-Stage-3-shake.webm?19ae6478\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH02-Stage-3-shake.webm?a31217fd\" type=\"video/webm\"\u003eVideo of the shake animation at the beginning of TH02's Stage 3, demonstrating how shaking VRAM by 16 pixels temporarily reveals parts of the tile area below the HUD. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH02-Stage-3-shake.avi?93b9beb7\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"217\" data-title=\"Tiles below HUD\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-09-06-TH02-Stage-3-shake-with-tiles.webp?fb0f12a6\" preload=\"none\" controls data-title=\"TRAM cleared\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"381\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-09-06-TH02-Stage-3-shake-with-tiles.avi?31df704e\"\u003e\u003csource src=\"/blog/static/video/av1/2025-09-06-TH02-Stage-3-shake-with-tiles.webm?11c785b4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-09-06-TH02-Stage-3-shake-with-tiles.webm?35fae786\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-09-06-TH02-Stage-3-shake-with-tiles.webm?a50c358a\" type=\"video/webm\"\u003eVideo of the shake animation at the beginning of TH02's Stage 3 with cleared TRAM, demonstrating how shaking VRAM by 16 pixels temporarily reveals parts of the tile area below the HUD. \u003ca href=\"/blog/static/video/zmbv/2025-09-06-TH02-Stage-3-shake-with-tiles.avi?31df704e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"217\" data-title=\"Tiles below HUD\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThis definitely counts as a bug to be fixed in this game's Anniversary Edition, but how would we fix this one on PC-98 where we do need the tile area in VRAM? Moving the tiles to another place and patching the \u003ca href=\"/blog/2023-03-30#diffs-2023-03-30\"\u003e📝 .MAP\u003c/a\u003e at runtime?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tApplying the palette to produce the final rendered image then raises another set of exciting engineering questions. Would we actually use a palettized 4bpp buffer in memory, storing two pixels in a byte? Perhaps with an 8-bit palette that maps each possible pair of pixels to a pair of 32-bit RGBA values, halving the amount of per-frame palette lookups? Or would we always store an RGBA image and merely offer a palettized API around it? As far as I'm concerned, these challenges are way more exciting than the prospect of locking ourselves into some shader language.\n\u003c/p\u003e\u003ch3 id=\"pages-2025-09-06\"\u003e📃 Page flipping 📃\u003c/h3\u003e\u003cp\u003e\n\tBut wait. If the port produces \u003ci\u003ea\u003c/i\u003e persistent graphics layer, shouldn't it produce \u003ci\u003etwo\u003c/i\u003e, one for each VRAM page on the PC-98? From the point of view of a modern port, we really don't need to. We only ever upload one \"VRAM page\" to the GPU anyway, which is then scrolled and scaled onto one of the GPU's backbuffers inside the \u003ca href=\"https://en.wikipedia.org/wiki/Swap_chain\"\u003eswapchain\u003c/a\u003e. Then, the game can immediately continue drawing onto the same software-rendered VRAM buffer in the next frame without affecting the GPU output.\n\u003c/p\u003e\u003cp\u003e\n\tObviously, this rendering paradigm doesn't translate back to the PC-98. There, we must render each frame to either the invisible or the visible page. Also, minimal redraw is crucial because we can neither afford the memory nor the performance to regularly copy an entire 128\u0026nbsp;KB of pixel data from whatever place to VRAM. As a result, page flips are a common sight in even the highest levels of menu and cutscene code, adding yet another unsightly piece of state you have to keep track of while reviewing and modding the code. I've grown to hate them quite a lot over the past four months because of just how often they are associated with bad code: In most menu and cutscene screens, ZUN just uses the second VRAM page as pixel storage for inter-page copies using the EGC, \u003ca href=\"/blog/2022-06-17\"\u003e📝 whose slowness is a regular topic on this blog\u003c/a\u003e. Once you've replaced these copies with optimized blits from conventional RAM, you've not only removed all these page flips and clearly revealed these screens as the single-buffered affairs they've always been, but you've also accelerated them enough to remove any screen tearing issues they might have had at 66\u0026nbsp;MHz.\n\u003c/p\u003e\u003cp\u003e\n\tUnfortunately, things are not that easy everywhere:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSometimes, menus and cutscenes do require involved page flipping tricks to cleanly switch between two screens without tearing.\u003c/li\u003e\n\t\u003cli\u003eBut a few of them are genuinely double-buffered. Their minimal redraw code must indeed always keep two alternating states of VRAM in mind, which effectively leaks a hardware detail – the length of the PC-98's \"swapchain\" – into the highest levels of game code.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tCan we rewrite all of these cases in a way that high-level game code no longer has to care about pages? Can we perhaps even banish page flipping to a new lower level of the architecture that all menus and cutscenes are built on top of, and thus unconditionally double-buffer every screen while still maintaining minimal redraw? Or is none of this worth it and we'll just live with two VRAM pages on all platforms? I'm honestly not sure. And that's just a small preview of the porting challenges that still await us and were far beyond the scope of even these 11 pushes…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs for the commits that are formally assigned to this blog post: It was all maintenance, build system setup, and some debloating work on TH01 around its packfile support that I thought would be necessary but thankfully didn't yet need after all. More about that in, you guessed it, \u003ca href=\"/blog/2025-09-29#fragment-2025-09-29\"\u003e📝 part 4\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tAlright! Improving performance, fixing screen tearing issues, establishing better cross-platform interfaces, and cleaning up ZUN's code to facilitate all of that… I've got a lot to do now. Next up: Getting closer to our performance goals by optimizing all PC-98-native code surrounding the .PI files used for backgrounds and cutscene pictures, since we later want to draw our TH03 netplay menus on top.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-08-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-09-06T03:01:24Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-08-12",
      "url": "https://rec98.nmlgc.net/blog/2025-08-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-05-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-08-12\"\u003e\u003ctime datetime=\"2025-08-12T14:00:00Z\"\u003e2025-08-12\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/policy-bugfix\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t\u003ci\u003eAnother\u003c/i\u003e replay desync bug in Shuusou Gyoku's Extra Stage that I accidentally introduced in \u003ca href=\"https://github.com/nmlgc/ssg/releases/tag/P0295\"\u003eP0295\u003c/a\u003e and that therefore falls under my \u003ca href=\"/faq#mod-bugs\"\u003efree bugfix policy\u003c/a\u003e?! Last time, we were lucky and there was a general solution that would allow automatic repairs for affected replays inside the game, but not this time. That's right, the most dreaded catastrophic case has actually happened, and I accidentally forked gameplay. ⚠️ If you've recorded an Extra Stage replay on any build I released in the last 10 months, there's a rare chance that your replay might be invalid and would desync on the original game and future builds.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#bug-2025-08-12\"\u003eAccidentally breaking Marisa's bit rotation\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#defense-2025-08-12\"\u003eDefense strategies\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"bug-2025-08-12\"\u003e\u003cp\u003e\n\tSo, what happened? \u003ca href=\"/blog/2024-10-22#cleanup-2024-10-22\"\u003e📝 Modernizing pbg's gameplay code for the sole sake of satisfying static analysis\u003c/a\u003e was a scary prospect from the start, despite the already reduced scope of warnings I went for. And sure enough, this bug \u003ci\u003ehad\u003c/i\u003e to be an issue with integer casting once again, specifically in the code that controls the rotation of Marisa's \u003ca href=\"/blog/2022-04-18#marisa-2022-04-18\"\u003e📝 bits\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre class=\"chroma\"\u003e// Calculate the angular velocity (`dir`) of a bit.\n// `bit-\u003eAngle` is the same kind of \u003ca href=\"/blog/2022-03-05\"\u003e📝 8-bit angle\u003c/a\u003e used in PC-98 Touhou.\nconst uint8_t d = ((BitData.BaseAngle \u003e\u003e 1) + (delta * bit-\u003eBitID));\n\n\u003cspan class=\"gi\"\u003e// Original pbg code\nint dir = (int)d - (int)(bit-\u003eAngle);\u003c/span\u003e\n\n\u003cspan class=\"gd\"\u003e// Broken modern rewrite in P0295\nint dir = (Cast::sign\u0026lt;int8_t\u0026gt;(d) - Cast::sign\u0026lt;int8_t\u0026gt;(bit-\u003eAngle));\u003c/span\u003e\n\n\u003cspan class=\"gi\"\u003e// Correct modern rewrite in P0310-2\nint dir = (Cast::up_sign\u0026lt;int\u0026gt;(d) - Cast::up_sign\u0026lt;int\u0026gt;(bit-\u003eAngle));\u003c/span\u003e\n\u003c/pre\u003e\n\t\u003cfigcaption\u003eWhy did I try to be smart here?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tLet's plug in \u003cvar\u003e44\u003c/var\u003e for \u003ccode\u003ed\u003c/code\u003e and \u003cvar\u003e172\u003c/var\u003e for \u003ccode\u003ebit-\u003eAngle\u003c/code\u003e. With the original \u003ccode\u003eint\u003c/code\u003e casts, this results in an obvious \u003ccode\u003edir\u003c/code\u003e of\n\u003c/p\u003e\u003cfigure\u003e\n\t(44\u0026nbsp;-\u0026nbsp;172)\u0026nbsp;= -128\n\u003c/figure\u003e\u003cp\u003e\n\tBut if an \u003ccode\u003eint8_t\u003c/code\u003e cast would turn \u003cvar\u003e172\u003c/var\u003e to \u003cvar\u003e-84\u003c/var\u003e, shouldn't a result of\n\u003c/p\u003e\u003cfigure\u003e\n\t(44\u0026nbsp;-\u0026nbsp;(-84))\u0026nbsp;= 128\n\u003c/figure\u003e\u003cp\u003e\n\tthen also overflow to -128 if the calculation is done in 8 bits? \u003ca href=\"https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r0.html\"\u003eAs of C++20, this overflow is even well-defined.\u003c/a\u003e Well, C \u003ci\u003estill\u003c/i\u003e promotes both operands to \u003ccode\u003eint\u003c/code\u003e and only \u003ci\u003ethen\u003c/i\u003e checks whether they're the same type, as specified in Section 6.3.1.8/1 of the C standard. Thus, we lose the expected overflow, and the bits begin to rotate in the other direction. That's now the third time that C's integer promotion rules have \u003ca href=\"/blog/2024-12-04#limit-2024-2024-12-04\"\u003e📝 ruined my day\u003c/a\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tIt's easy to see how a difference in rotation can lead to different gameplay by changing the spawn points of bullets, but this bug actually tends to have far bigger consequences. At one point during the fight, the ECL script reads out the bit angle and not only applies it to Marisa's own movement angle, but also uses it to determine the next danmaku pattern. And with a large enough difference compared to pbg's original intentions, we get the reported desync:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-08-12-SH01-Ripper-Roo-bug-broken.webp?9c0b2297\" preload=\"none\" controls data-title=\"Broken/desynced gameplay\" loop data-active width=\"384\" height=\"480\" data-fps=\"62.5\" data-frame-count=\"397\" style=\"aspect-ratio: 384 / 480\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-08-12-SH01-Ripper-Roo-bug-broken.avi?a0e8ea80\"\u003e\u003csource src=\"/blog/static/video/av1/2025-08-12-SH01-Ripper-Roo-bug-broken.webm?d31bf5a9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-08-12-SH01-Ripper-Roo-bug-broken.webm?b29caceb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-08-12-SH01-Ripper-Roo-bug-broken.webm?32eeeb5a\" type=\"video/webm\"\u003eVideo of the gameplay-forking bug that the ReC98 P0925 build introduced into Shuusou Gyoku's Extra Stage, resulting in a pattern that differs from what this previously recorded replay expects. \u003ca href=\"/blog/static/video/zmbv/2025-08-12-SH01-Ripper-Roo-bug-broken.avi?a0e8ea80\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-08-12-SH01-Ripper-Roo-bug-correct.webp?f5593fd9\" preload=\"none\" controls data-title=\"Expected gameplay\" loop width=\"384\" height=\"480\" data-fps=\"62.5\" data-frame-count=\"397\" style=\"aspect-ratio: 384 / 480\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-08-12-SH01-Ripper-Roo-bug-correct.avi?1582d32d\"\u003e\u003csource src=\"/blog/static/video/av1/2025-08-12-SH01-Ripper-Roo-bug-correct.webm?48e40be3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-08-12-SH01-Ripper-Roo-bug-correct.webm?5a9ffe3e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-08-12-SH01-Ripper-Roo-bug-correct.webm?bd34bdcd\" type=\"video/webm\"\u003eVideo of the expected gameplay behavior that the bug-triggering Shuusou Gyoku Extra Stage replay was recorded with. \u003ca href=\"/blog/static/video/zmbv/2025-08-12-SH01-Ripper-Roo-bug-correct.avi?1582d32d\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSince a lot of Marisa's behavior and pattern selection comes down to RNG, this doesn't necessarily \u003ci\u003ehave\u003c/i\u003e to happen. While most fights will run into a case where this bug would change bit direction for a few frames, \u003ci\u003epattern\u003c/i\u003e differences like this one appear to be rather rare. None of my six Marisa-beating test replays desync in response to this bug, which explains how it remained undetected during all my testing.\u003cbr\u003e\n\tThankfully, the replay above was recorded on \u003ca href=\"https://github.com/nmlgc/ssg/releases/tag/P0275\"\u003eP0275\u003c/a\u003e. Thus, it relied on the original behavior, and the replay will play back correctly again on the new and future builds. So far, I haven't seen any replay that relies on the wrong behavior; even \u003ca href=\"https://www.youtube.com/watch?v=OjkJiEHArcI\"\u003eBWF6's crazy god-tier RNG run that got all the easy patterns\u003c/a\u003e remains legit. But the possibility definitely exists.\n\u003c/p\u003e\u003chr id=\"defense-2025-08-12\"\u003e\u003cp\u003e\n\tThe only true defense against this kind of RNG-dependent gameplay fork involves validation against tons and tons of replays. Unfortunately, Shuusou Gyoku's original single-replay system makes collecting them too impractical for everyone involved. And as long as the backers focus on flashier features over \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/37\"\u003etestability\u003c/a\u003e, we'll always run the risk of accidental gameplay forks, despite my best efforts and promises.\u003cbr\u003e\n\tStill, the obvious and actionable lesson for myself is that I have to get better at not touching gameplay code. And as a first step of that, I will keep the fix at this single line instead of trying to look for and fix more of these potential issues by throwing more static analysis onto pbg's code.\n\u003c/p\u003e\u003cp\u003e\n\tThis bug could have been prevented by having at least a basic physical split of gameplay and rendering code into different source files, which would allow the former to be excluded from these massive modernizing refactors more easily. Sure, TH02 and especially TH01 intertwine gameplay and rendering so much that this would have been another subproject consuming multiple pushes. But for Shuusou Gyoku, it would have been easy, and I still didn't do it. Big mistake. I'm definitely going to take the time once we get to Kioh Gyoku.\u003cbr\u003e\n\tIn that light, it's a massive advantage that I'll have to implement TH03 netplay \u003ci\u003ewithout\u003c/i\u003e \u003ccode\u003eMAIN.EXE\u003c/code\u003e being position-independent. This way, the binary diff against the original version remains small by necessity, and the risk of accidental gameplay forks is massively reduced.\n\u003c/p\u003e\u003cp\u003e\n\tMany, \u003ci\u003emany\u003c/i\u003e thanks to Ripper Roo for reporting this bug and providing the one affected replay. Let's hope that this was the one and only bug hiding in these guideline rewrites…\u003cbr\u003e\n\tWe're lucky to have found this one \u003ci\u003ebefore\u003c/i\u003e I implemented the upcoming \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/36\"\u003ebetter and more community-usable replay system\u003c/a\u003e. Imagine if we already had a replay site that hosted a bunch of replays recorded on the broken builds. We'd now have to mark all of them as forked, \u003ci\u003eexcept\u003c/i\u003e that we probably want to manually validate and remove that fork marker for every replay that syncs on pbg's original build after all… That would have been even more bureaucracy waiting for me now.\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0310-2\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01n-032.png?38cbcd51\" alt=\":sh01n:\" width=\"24\" height=\"24\" \n\t\t\tsrcset=\"/static/emoji-sh01n-016.png?b4ffeada 0.66x, /static/emoji-sh01n-032.png?38cbcd51 1.33x, /static/emoji-sh01n-048.png?67a1addc 2x, /static/emoji-sh01n-128.png?96524b5d 5.33x\"\u003e Shuusou Gyoku P0310-2 Windows build\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://aur.archlinux.org/packages/seihou-shuusou-gyoku\"\u003eShuusou Gyoku P0310-2 on the AUR\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://flathub.org/apps/net.nmlgc.rec98.sh01\"\u003eShuusou Gyoku P0310-2 on Flathub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAt least the next regular PC-98 Touhou delivery doesn't even try to touch game code. It's been feature-complete for a while and is only missing the blog post, some in-depth testing, and the usual release preparations. As you might have guessed from the time it took, this first foray into portability escalated to another big 10-push monster… Really happy with the visualizations this time around, though.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-09-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-05-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-08-12T16:00:00+02:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-05-20",
      "url": "https://rec98.nmlgc.net/blog/2025-05-20",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-08-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-05-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-05-20\"\u003e\u003ctime datetime=\"2025-05-20T14:00:00Z\"\u003e2025-05-20\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/policy-bugfix\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/replay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Gameplay inputs stored in files that can be exchanged between players.\"\u003ereplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tUnfortunately, I have to quickly interrupt the current PC-98 Touhou progress with breaking news of a replay desync bug in my Shuusou Gyoku build. Yup, it's \u003ca href=\"/faq#mod-bugs\"\u003efree mod bugfix time\u003c/a\u003e again, this time featuring a bug with the most complex implications so far…\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#bug-2025-05-20\"\u003eThe Extra Stage replay desync bug introduced in P0295\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#how-2025-05-20\"\u003eHow it happened\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#fix-ideas-2025-05-20\"\u003eThinking about the best way to fix it\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#filesizes-2025-05-20\"\u003eAnother filesize-related bug in replay saving\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"bug-2025-05-20\"\u003e\u003cp\u003e\n\tThe bug in question dates back to the P0295 build from last October. While that giant release mostly focused on porting the game's rendering to SDL, it also included \u003ca href=\"/blog/2024-10-22#hotkeys-2024-10-22\"\u003e📝 fixes for three pbg bugs in Shuusou Gyoku's handling of Extra Stage replays\u003c/a\u003e. Unfortunately, these fixes would introduce a bug of my own that was even worse. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tEver since that build, \u003ca href=\"https://github.com/nmlgc/ssg/blob/3829b6d1f20d5279c50d7dbc9daaf3a111fc5e52/GIAN07/DEMOPLAY.H#L20-L41\"\u003ethe replay header\u003c/a\u003e has consistently stored the difficulty and starting life count as shown in the \u003ci\u003eConfig → Difficulty\u003c/i\u003e menu. This looks fine on the surface until you consider the exact issue behind the three bugs: Shuusou Gyoku's Extra Stage is supposed to run on Hard difficulty and 2 starting lives, \u003ci\u003enot\u003c/i\u003e on whatever you set for the regular 6 stages.\u003cbr\u003e\n\tYou can probably already imagine how invalid difficulty settings will cause desyncs shortly into a replay. Running a debug build at any commit between ≥P0295 and ≤P0310 quickly reveals the issue:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 512px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-20-SH01-Rank-Extra-at-start.webp?b9104104\"\n\t\tdata-title=\"Regular Extra difficulty/rank/lives\"\n\t\twidth=\"512\"\n\t\talt=\"Screenshot of Shuusou Gyoku's debug mode at the beginning of the Extra Stage, showing off the intended internal difficulty (Hard) and initial rank value (Pr = 10240).\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-20-SH01-Rank-Extra-for-P0295-replay.webp?668c4a8b\"\n\t\tclass=\"active\"\n\t\tdata-title=\"Difficulty/rank/lives in a \u0026quot;Lunatic\u0026quot; replay\"\n\t\twidth=\"512\"\n\t\talt=\"Screenshot of Shuusou Gyoku's debug mode at the beginning of an Extra Stage replay recorded on a ReC98 build between ≥P0295 and ≤P0310, showing off how the replay incorrectly uses the configured difficulty (Lunatic) along with the higher rank that comes along with it.\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tDifferent difficulties come with different initial rank values (\u003ccode\u003ePr\u003c/code\u003e), which cause bullets and lasers to spawn at different speeds than what the player maneuvered around while recording the replay, which in turn will manifest as a desync.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe only way to protect a replay from this bug was to set the regular game to Hard difficulty and 2 starting life settings in the \u003ci\u003eConfig → Difficulty\u003c/i\u003e menu before recording. This is probably one of the rarest configurations imaginable – most people will have set the difficulty to either Normal to get that survival clear that unlocks Extra in the first place, or to Lunatic because they're superplayers and that's the only difficulty that matters to them. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tNote how the bug only affects the saved replay file. You were still \u003ci\u003eplaying\u003c/i\u003e and recording Extra in its intended Hard difficulty and with 2 starting lives, and any clear you've achieved was still valid.\n\u003c/p\u003e\u003chr id=\"how-2025-05-20\"\u003e\u003cp\u003e\n\tThis is exactly the kind of bug that can easily fall through the cracks of the regular testing that my backers and I do for every new build. Replays are a key item on my testing checklist, but I primarily test whether \u003ci\u003eexisting\u003c/i\u003e ones still work. With only one replay slot per stage, recording a new replay is always a cumbersome process: Is my previous replay for that stage worth keeping? If yes, what made it special? After all, I now need to give a more descriptive name to the file. Do I remember, or do I have to watch the replay again?\u003cbr\u003e\n\tAlso, the primary concern of replays is compatibility with pbg's original 1.005 version. In that context, they can provide important evidence that I haven't accidentally forked the gameplay. Therefore, replays should hit as many gameplay aspects and potential failure/desync points as possible, which requires actual gameplay effort. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e From that point of view, it makes more sense to just keep testing with existing replays, \u003ci\u003eespecially\u003c/i\u003e when it comes to the Extra Stage.\n\u003c/p\u003e\u003chr id=\"fix-ideas-2025-05-20\"\u003e\u003cp\u003e\n\tSince this was just a metadata issue, we can both \u003ca href=\"https://github.com/nmlgc/ssg/commit/6dbce202fa32f3ca3773a1653fc117d97470cedb\"\u003eeasily fix this bug for future replays\u003c/a\u003e \u003ci\u003eand\u003c/i\u003e repair any existing affected ones. We simply have to set the replay's difficulty and starting lives to the one and only official values for the Extra Stage, and they will play back correctly again.\n\u003c/p\u003e\u003cp\u003e\n\tBut doing that creates a potential problem. What if you actually modded the game before P0295, intentionally changed the difficulty and/or number of starting lives for the Extra Stage, and then recorded a replay? If that was the whole extent of your mod, such a replay would play back correctly on not just your modded build of Shuusou Gyoku, but on every single one of my builds \u003ci\u003eand\u003c/i\u003e pbg's original 1.005 build. \"Fixing\" these settings in the replay header would then actually break such a replay. Since we're still using pbg's old replay format, there is no way we can distinguish valid modded replays from broken and desyncing ones by looking at just the replay header.\u003cbr\u003e\n\tWe \u003ci\u003ecould\u003c/i\u003e tell \u003ci\u003eafter\u003c/i\u003e we've run the replay – if the game ends before the replay has reached its last recorded frame, we know that \u003ci\u003esomething\u003c/i\u003e is wrong. However, we're not quite at the point where we can \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/37\"\u003equickly simulate an entire round of gameplay logic on just the CPU, without rendering anything\u003c/a\u003e. The best we could do until then is to pop up a message at the end of a rendered replay, informing the player that they've just watched a desync and offering an automatic repair of known issues. But that would be a lot of work for a \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/policy-bugfix\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/a\u003e\u003c/span\u003e, and even fall under the planned paid feature of \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/17\"\u003eimproved replay-related error reporting\u003c/a\u003e. And if we zoom out, such a window won't be much of a help in the general case of people watching replays from incompatible builds. The game can't possibly know the specific mod a desyncing replay originated from, so what could it possibly do, other than to say \u003ci\u003e\"Sorry, that was, in fact, a desync 🤷\"\u003c/i\u003e?\u003cbr\u003e\n\tThat's why it's so important to me that \u003ca href=\"/blog/2025-04-09#tag-2025-04-09\"\u003e📝 the new replay format stores the exact game binary and stage script versions a replay was recorded on\u003c/a\u003e. As well as any gameplay tweaking options, if we ever go that route: \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/38\"\u003eProperly fixing Shuusou Gyoku's fake deathbomb quirk\u003c/a\u003e is not just about \u003ca href=\"https://github.com/Tasos500/ssg/compare/d766e6f...751e213\"\u003ethe few lines of custom gameplay code you can find in Tasos500's fork\u003c/a\u003e, but mainly about the bureaucracy of cleanly establishing a separate competition tier, not breaking existing replays, and making sure that \u003ca href=\"https://github.com/n-rook/thscoreboard/issues/505\"\u003ereplay hosting sites\u003c/a\u003e deal with the distinction as well.\n\u003c/p\u003e\u003cp\u003e\n\tThat said, that's a lot of thought for a very specific potential scenario. Any change to the Extra Stage settings would have required modding the game at either the C++ or machine code level. If you were able to pull \u003ci\u003ethat\u003c/i\u003e off \u003ci\u003eand\u003c/i\u003e you're considering updating to the new build, you'll probably also read these lines and will have no problem adapting whatever fix I roll out for that issue.\n\u003c/p\u003e\u003chr id=\"filesizes-2025-05-20\"\u003e\u003cp\u003e\n\tSo, let's go for that unconditional rewrite of every affected Extra Stage replay upon clicking the \u003ccode lang=\"ja\"\u003eExStage デモ再生\u003c/code\u003e option… but wait, why are the rewritten files suddenly smaller than the old ones? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tTurns out that there was \u003ci\u003eanother\u003c/i\u003e replay-related bug that dates back to \u003ca href=\"/blog/2022-09-04\"\u003e📝 my very first Shuusou Gyoku release from September 2022\u003c/a\u003e. This one boiled down to the classic C/C++ footgun of \u003ca href=\"https://github.com/nmlgc/ssg/commit/3ce52b0cc0df9776f8fab70f6f174e3e2b769e72\"\u003econfusing byte sizes with element counts\u003c/a\u003e, but \u003ca href=\"https://github.com/nmlgc/ssg/commit/a9d13a11bd5dc13079d8a9ee3027e558f83fd840\"\u003epbg's misleading variable names\u003c/a\u003e certainly played their part as well.\u003cbr\u003e\n\tThis bug is mostly harmless within the unmodded game, which also explains how I didn't detect it for so long. The game doesn't care about compressing and uncompressing twice as many bytes, the loader still copied the correct amount of bytes and wouldn't have overflowed the buffer, and at a few KB per replay, it doesn't really stick out if the files are roughly twice as large as they needed to be. But this was still a landmine that would have exploded once modders crafted stages longer than half of the \u003ca href=\"https://github.com/nmlgc/ssg/blob/3829b6d1f20d5279c50d7dbc9daaf3a111fc5e52/GIAN07/DEMOPLAY.H#L16\"\u003e20-minute buffer that pbg designated for replays\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tSince I'm already implementing an automated fix here, I might as well also recompress every watched non-Extra Stage replay if its amount of decompressed bytes doesn't match the replay's indicated frame count. Of course, recompression won't work if you've marked the replay files as read-only, which I often do as a means of protecting them from getting overwritten with accidentally recorded new replays of the same stage…\u003cbr\u003e\n\t…but wait, how about restricting \u003ci\u003eboth\u003c/i\u003e fixes to writable replay files? This would create at least one possibility of protecting existing modded replays, and also make sense from a consistency point of view. If the game isn't allowed to fix the replay, it also shouldn't untransparently hotpatch its header and play it differently than any other build of the game would play it, even if that way was the correct one in the vast majority of cases. Sure, this is slightly annoying for people who use that same trick, but those people will probably also read either these lines or the release notes.\n\u003c/p\u003e\u003cp\u003e\n\tAs a neat bonus, I also made sure to preserve the original timestamps of any repaired and/or recompressed replay file. This is the only other piece of meaningful identifying metadata we have with these files, and I don't want to throw it away just because I messed up the saving code at one point. Without that extra level of care, I probably wouldn't have gone for such an unconditional automatic fix in the first place. Instead, this little detail makes the whole fix as invisible as it could possibly be. If you only recorded an Extra Stage replay once, haven't watched it since, and haven't touched the file either, you won't even notice that there was a bug in the first place.\u003cbr\u003e\n\t\u003ca href=\"https://wiki.libsdl.org/SDL3/CategoryFilesystem\"\u003eSDL 3's filesystem API\u003c/a\u003e does not cover file timestamp modification, so this required \u003ca href=\"https://github.com/nmlgc/ssg/commit/5fbcb3029a29076a239de60988f2067bf7b0165d\"\u003emore OS-specific code\u003c/a\u003e of the kind \u003ca href=\"/blog/2025-04-25#sdl-2025-04-25\"\u003e📝 I'd rather want to get rid of\u003c/a\u003e. \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_GetPathInfo\"\u003eSDL 3 does support timestamp \u003ci\u003eretrieval\u003c/i\u003e\u003c/a\u003e though, and that's all we need for \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/36\"\u003ethe new replay format\u003c/a\u003e where I'll take the timestamp from the filesystem and properly write it into the file itself.\n\u003c/p\u003e\u003cp\u003e\n\tAnd there we go, no more replay bugs! Also, did I just write down all the justification anyone would ever need for the new replay format? That should be shortening that future blog post by quite a bit at least…\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0310-1\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01n-032.png?38cbcd51\" alt=\":sh01n:\" width=\"24\" height=\"24\" \n\t\t\tsrcset=\"/static/emoji-sh01n-016.png?b4ffeada 0.66x, /static/emoji-sh01n-032.png?38cbcd51 1.33x, /static/emoji-sh01n-048.png?67a1addc 2x, /static/emoji-sh01n-128.png?96524b5d 5.33x\"\u003e Shuusou Gyoku P0310-1 Windows build\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://aur.archlinux.org/packages/seihou-shuusou-gyoku\"\u003eShuusou Gyoku P0310-1 on the AUR\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://flathub.org/apps/net.nmlgc.rec98.sh01\"\u003eShuusou Gyoku P0310-1 on Flathub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThanks to \u003ca href=\"https://boards.4chan.org/jp/thread/49311611/touhou-gameplay-thread#p49320040\"\u003e\u0026gt;\u0026gt;49320040 on /jp/\u003c/a\u003e for pointing out that desyncs exist. Please tell me this sort of thing! I'm not ZUN, desyncs are critical bugs that will always receive my immediate attention. If they turn out to be my fault, they definitely fall under my free bugfix policy, and if they don't, we at least get to document them as bugs in the original game that might get fixed in a later push.\u003cbr\u003e\n\tAlright, back to writing blitters for the PC-98…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-08-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-05-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-05-20T16:00:00+02:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-05-10",
      "url": "https://rec98.nmlgc.net/blog/2025-05-10",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-05-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-04-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-05-10\"\u003e\u003ctime datetime=\"2025-05-10T20:41:57Z\"\u003e2025-05-10 20:41\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0311\"\u003eP0311\u003c/a\u003e\n\t\t\tTH03 decompilation (Main menu, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/165f090...9b2d7eb\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0312\"\u003eP0312\u003c/a\u003e\n\t\t\tTH03 decompilation (Main menu, part 2/2 + Static HUD parts) + TH02 PI (Stage 3 midboss/boss state) + TH03 PI (Technical 100% for MAINL.EXE)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9b2d7eb...f6d836b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://www.youtube.com/@32th\"\u003e32th System\u003c/a\u003e, [Anonymous], iruleatgames, Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gaiji\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\tfigure.main-menu-2025-05-10 img {\n\t\tbackground-image: url('/static/th03-title.webp?6c8403f7');\n\t}\n\n\t.demo-2025-05-10 tbody th {\n\t\ttext-align: right;\n\t}\n\n\t.demo-2025-05-10 tbody td:last-child {\n\t\ttext-align: right;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tSurprise! The last missing main menu in PC-98 Touhou was, in fact, not that hard. Finishing the rest of TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e took slightly shorter than the expected 2 pushes, which left enough room to uncover an unexpected mystery and take important leaps in position independence…\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#menu-2025-05-10\"\u003eTH03's main/option menu\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#unused-2025-05-10\"\u003eUnused labels\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#story-2025-05-10\"\u003ePicking opponents for Story Mode\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#demo-2025-05-10\"\u003eDemo Play difficulty?\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#bal-2025-05-10\"\u003eBoss Attack level confusion\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tuning-2025-05-10\"\u003eThe true nature of difficulty in TH03\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-pi-2025-05-10\"\u003eMore variables from TH02's Stage 3\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#ports-2025-05-10\"\u003eTechnical position independence for TH03's \u003ccode\u003eMAINL.EXE\u003c/code\u003e\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"menu-2025-05-10\"\u003e\u003cp\u003e\n\tFor TH03, ZUN stepped up the visual quality of the main menu items by exchanging TH02's monospaced font with fixed, pre-composited strings of proportional text. While TH04 would later place its menu text in VRAM, TH03 still wanted to stay with TH02's approach of using \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e to display the menu items on the PC-98 text layer. Since gaiji have a fixed size of 16×16 pixels, this requires the pre-composited bitmaps to be cut into blocks of that size and padded with blank pixels as necessary:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 544px;\"\u003e\n\t\u003cfigcaption\u003e\n\t\t\u003cform\u003e\n\t\t\t\u003cinput type=\"checkbox\" id=\"grid-toggle-2025-05-10\" checked onchange=\"\n\t\t\t\tconst gaiji = document.getElementById('mikoft-2025-05-10');\n\t\t\t\tconst grid = document.getElementById('mikoft-grid-2025-05-10');\n\t\t\t\tif(this.checked) {\n\t\t\t\t\tgrid.hidden = false;\n\t\t\t\t\tgaiji.hidden = true;\n\t\t\t\t} else {\n\t\t\t\t\tgaiji.hidden = false;\n\t\t\t\t\tgrid.hidden = true;\n\t\t\t\t}\n\t\t\t\"\u003e\n\t\t\t\u003clabel for=\"grid-toggle-2025-05-10\"\u003eShow gaiji ID grid\u003c/label\u003e\n\t\t\u003c/form\u003e\n\t\u003c/figcaption\u003e\n\t\u003cdiv class=\"multilayer\" style=\"aspect-ratio: 17 / 8;\"\u003e\n\t\t\u003cimg\n\t\t\tid=\"mikoft-2025-05-10\"\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-MIKOFT-proportional-text.webp?88fea30a\"\n\t\t\talt=\"The proportional text section from TH03's MIKOFT.BFT\"\n\t\t\twidth=\"544\"\n\t\t\thidden\n\t\t\u003e\n\t\t\u003cimg\n\t\t\tid=\"mikoft-grid-2025-05-10\"\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-MIKOFT-proportional-text-with-grid.webp?1c0a1d24\"\n\t\t\talt=\"The proportional text section from TH03's MIKOFT.BFT, with the 16×16 gaiji grid overlaid\"\n\t\t\twidth=\"544\"\n\t\t\u003e\n\t\u003c/div\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf your combined amount of text is short enough to fit into the PC-98's 256 gaiji slots, this is a nice way of using hardware features to replace the need for a proportional text renderer. It especially simplifies transitions between menus – simply wiping the entire TRAM is both cheap and certainly less error-prone than (un)blitting pixels in VRAM, which \u003ca href=\"/blog/2023-11-30#main-2023-11-30\"\u003e📝 ZUN was always kind of sloppy at\u003c/a\u003e.\u003cbr\u003e\n\tHowever, all this text still needs to be composited and cut into gaiji \u003ci\u003esomewhere\u003c/i\u003e. If you do that manually, it's easy to lose sight of how the text is supposed to appear on screen, especially if you decide to horizontally center it. Then, you're in for some awkward coordinate fiddling as you try to place these 16-pixel bricks into the 8-pixel text grid to somehow make it all appear centered:\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cfigure class=\"pixelated\" style=\"width: 272px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Main-menu-original.webp?94f664ca\"\n\t\t\tclass=\"active\"\n\t\t\tdata-title=\"Original\"\n\t\t\twidth=\"272\"\n\t\t\talt=\"TH03's main menu box as it appears in the original game\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Main-menu-original-bricks.webp?7f35b106\"\n\t\t\tdata-title=\"Bricks\"\n\t\t\twidth=\"272\"\n\t\t\talt=\"TH03's main menu box with opaque gaiji to highlight their exact locations in TRAM\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Main-menu-centered.webp?97be39f0\"\n\t\t\tdata-title=\"Correct\"\n\t\t\twidth=\"272\"\n\t\t\talt=\"TH03's main menu box with correct centering\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003c/figure\u003e\u003cfigure class=\"pixelated\" style=\"width: 480px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-original.webp?e5ced17f\"\n\t\t\tclass=\"active\"\n\t\t\tdata-title=\"Original\"\n\t\t\twidth=\"480\"\n\t\t\talt=\"TH03's Option menu box as it appears in the original game\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-original-bricks.webp?a857fb47\"\n\t\t\tdata-title=\"Bricks\"\n\t\t\twidth=\"480\"\n\t\t\talt=\"TH03's Option menu box with opaque gaiji to highlight their exact locations in TRAM\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-centered.webp?af7aa995\"\n\t\t\tdata-title=\"Correctly centered\"\n\t\t\twidth=\"480\"\n\t\t\talt=\"TH03's Option menu box with correct centering\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003c/figure\u003e\n\t\u003cfigcaption\u003e\n\t\tThe VS Start menu actually is correctly centered.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThen again, did ZUN \u003ci\u003eactually\u003c/i\u003e want to center the Option menu like this? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e Even the main menu looks kind of uncanny with perfect centering, probably because I'm so used to the original. Imperfect centering usually counts as a bug, but this case is quirky enough to leave it as is. We might want to perfectly center any future translations, but that would definitely cost a bit as I'd then actually need to write that proportional text renderer.\u003cbr\u003e\n\tApart from that, we're left with only a very short list of actual bugs and landmines:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ci\u003eThe Cancel key is not handled inside the VS menu, arrgghh…\u003c/i\u003e! 🤬\u003c/li\u003e\n\t\u003cli\u003eZUN \u003ci\u003ealmost\u003c/i\u003e managed to write a title screen and menu without a \u003ca href=\"/blog/2024-02-03#mess-2024-02-03\"\u003e📝 screen\u003c/a\u003e \u003ca href=\"/blog/2024-11-22#tearing-2024-11-22\"\u003e📝 tearing\u003c/a\u003e landmine, but a single one still managed to sneak into the first frame of the title screen's short fade-in animation. This one will blow up when returning from the Music Room, and can be entirely blamed on that screen's choice to leave \u003ca href=\"/blog/2024-02-03#b-2024-02-03\"\u003e📝 a purple color in hardware palette slot 0\u003c/a\u003e. Replacing that color with black before returning would have completely hidden the potential tearing.\u003cbr\u003e\n\tThere might be another one in the long sliding animation, but I can only tell for sure once I've fully decompiled \u003ccode\u003eMAINL.EXE\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWhile the rest of the code is not free of the usual nitpicks, those don't matter in the grand scheme of things. The code for the sliding \u003cspan\n\tstyle=\"color: #909\"\u003e東方\u003c/span\u003e\u003cspan\n\tstyle=\"color: #f44\"\u003e夢\u003c/span\u003e\u003cspan\n\tstyle=\"color: #909\"\u003e時空\u003c/span\u003e\n\tanimation is even better: it makes decent use of the EGC and page flipping, and places the \u003ca href=\"/blog/2024-11-22#select-2024-11-22\"\u003e📝 loading calls for the character selection portraits\u003c/a\u003e at sensible points where the animation naturally wants to have a delay anyway. We're definitely ending the main menus of PC-98 Touhou on a high note here.\n\u003c/p\u003e\u003chr id=\"unused-2025-05-10\"\u003e\u003cp\u003e\n\tYou might have already spotted some unfamiliar text in the gaiji above, and indeed, we've got three pieces of \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e text in these two menus! Starting from the top, the \u003cimg src=\"data:image/webp;base64,UklGRnQAAABXRUJQVlA4TGcAAAAvNkADEA8QEfMfwnEAAGZzv3AErWC8klVS28aPA3UC99cdbNtuZ4jof2RKgAE6KLT+103X68pEN0P322mgvQo8MQ9M5Q6PT/WrdnzpelqlPnEK+s683LpCNIqkV2fw++Zke6c7S5MOAA==\" alt=\"Watch\"\u003e label is entirely unused as none of its gaiji IDs are referenced anywhere in the final code. The label's placement within the gaiji IDs would imply that this option was once part of the main menu, but nothing in the game suggests that the main menu ever had a bigger box that could fit a 7\u003csup\u003eth\u003c/sup\u003e element. On the contrary, every piece of menu code assumes that the box sprites loaded from \u003ccode\u003eOPWIN.BFT\u003c/code\u003e are exactly 128 pixels high:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 188px;\"\u003e\n\t\u003cimg src=\"/blog/static/2025-05-10-TH03-OPWIN.BFT.webp?13ba47dc\" width=\"96\" alt=\"TH03's OPWIN.BFT\"\u003e\n\t\u003cfigcaption\u003e\n\t\tFun fact: The code doesn't even use the 16 pixels in the middle, and instead just assumes that the pixels between the X coordinates of [﻿8;\u0026nbsp;16﻿[ and [﻿32;\u0026nbsp;40﻿[ are identical.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe unused MIDI music option has already been \u003ca href=\"https://tcrf.net/Touhou_Yumejikuu:_The_Phantasmagoria_of_Dim.Dream\"\u003ewidely documented elsewhere\u003c/a\u003e. Changing the first byte in \u003ccode\u003eYUME.CFG\u003c/code\u003e to \u003ccode\u003e02\u003c/code\u003e has no functional effect because ZUN removed most MIDI-related code before release. He did forget a few instances though, and \u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6d836b3a3142543830b519792f11938030863e4/th03/op_01.cpp#L611-L612\"\u003ethe surviving dedicated \u003ccode\u003eswitch\u003c/code\u003e case in the Option menu\u003c/a\u003e is now the entire reason why you can reveal this text without modifying the binary. Changing the option will always flip its value back to either \u003cvar\u003eoff\u003c/var\u003e or \u003cvar\u003eFM(86)\u003c/var\u003e.\u003cbr\u003e\n\tLast but not least, we have the \u003cimg src=\"data:image/webp;base64,UklGRmIAAABXRUJQVlA4TFUAAAAvKsADEEDQtm3Mn/bb7T+CiPmfAMe8/ioq/wYtiuIZjBtHFbCOKCYAFRVAXZhAiYG4IHihbzz4AsUbzCcqwcMXjDKoh7pBSQQAKxsn08RYUblxVZWnAA==\" alt=\"Type\" style=\"vertical-align: middle;\"\u003e label and its associated numbers. These are the most interesting ones in my eyes; nobody talks about them, even though we have definite proof that they were used for the KeyConfig options at some earlier point in development:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated bglayer main-menu-2025-05-10\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-KeyConfig-Type-1.webp?05c92a1e\"\n\t\tclass=\"active\"\n\t\tdata-title=\"Type 1 (Key vs. Key)\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH03's Option menu, showing the initial 'Type 1' label for the Key vs. Key option\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-KeyConfig-Type-2.webp?84473231\"\n\t\tdata-title=\"Type 2 (Joy vs. Key)\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH03's Option menu, showing the initial 'Type 2' label for the Joy vs. Key option\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Option-menu-KeyConfig-Type-3.webp?5bdfe02d\"\n\t\tdata-title=\"Type 3 (Key vs. Joy)\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of TH03's Option menu, showing the initial 'Type 3' label for the Key vs. Joy option\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut how exactly can we prove this? The code does \u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6d836b3a3142543830b519792f11938030863e4/th03/op_01.cpp#L495-L498\"\u003estring together the respective gaiji IDs and defines the resulting arrays right next to the final KeyConfig types\u003c/a\u003e, but doesn't use these arrays anywhere. By itself, this only means that these labels were associated with \u003ci\u003esome\u003c/i\u003e option that may have existed at one point in development. The proof must therefore come from outside the code – and in this case, it comes from both \u003ca href=\"https://en.touhouwiki.net/wiki/Phantasmagoria_of_Dim.Dream/Translation/Manual\"\u003e\u003ccode\u003e夢時空.TXT\u003c/code\u003e\u003c/a\u003e and \u003ca href=\"https://en.touhouwiki.net/wiki/Phantasmagoria_of_Dim.Dream/Translation/Other#%E5%A4%A2%E6%99%82%E7%A9%BA_1.TXT_(Yumejikuu_1.TXT)\"\u003e\u003ccode\u003e時空_1.TXT\u003c/code\u003e\u003c/a\u003e, which still refer to the KeyConfig options as numbered types:\n\u003c/p\u003e\u003cblockquote lang=\"ja\"\u003e　■６．操作方法\n[…]\n　　　ＦＭ音源のジョイスティックが無い場合は、ＴＹＰＥ１にしてください。\n\n　　　○ＴＹＰＥ１　Ｋｅｙ　ｖｓ　Ｋｅｙ\n[…]\n　　　○ＴＹＰＥ２　Ｊｏｙ　ｖｓ　Ｋｅｙ\n[…]\n　　　○ＴＹＰＥ３　Ｋｅｙ　ｖｓ　Ｊｏｙ\u003c/blockquote\u003e\u003chr id=\"story-2025-05-10\"\u003e\u003cp\u003e\n\tThat's all I've got about the menus, so let's talk characters and gameplay! When playing Story Mode, \u003ccode\u003eOP.EXE\u003c/code\u003e picks the opponents for all stages immediately after the \u003ca href=\"/blog/2024-11-22\"\u003e📝 Select screen\u003c/a\u003e has faded out. Each character fights a fixed and hardcoded opponent in Stage 7's Decisive Match:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable style=\"text-align: left;\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\u003cth\u003ePlayer\u003c/th\u003e\u003cth\u003eStage 7 opponent\u003c/th\u003e\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc1.webp?c3a345a5\" alt=\"Mima\" width=\"24\" height=\"24\"\u003e Mima\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc1.webp?c3a345a5\" alt=\"Mima\" width=\"24\" height=\"24\"\u003e Mima\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc2.webp?18385020\" alt=\"Marisa\" width=\"24\" height=\"24\"\u003e Marisa\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc3.webp?9101ba3a\" alt=\"Ellen\" width=\"24\" height=\"24\"\u003e Ellen\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc2.webp?18385020\" alt=\"Marisa\" width=\"24\" height=\"24\"\u003e Marisa\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc4.webp?91a4fd1b\" alt=\"Kotohime\" width=\"24\" height=\"24\"\u003e Kotohime\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc5.webp?cba094a2\" alt=\"Kana\" width=\"24\" height=\"24\"\u003e Kana\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc3.webp?9101ba3a\" alt=\"Ellen\" width=\"24\" height=\"24\"\u003e Ellen\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc6.webp?bfdfae8f\" alt=\"Rikako\" width=\"24\" height=\"24\"\u003e Rikako\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc5.webp?cba094a2\" alt=\"Kana\" width=\"24\" height=\"24\"\u003e Kana\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc7.webp?c77b71c0\" alt=\"Chiyuri\" width=\"24\" height=\"24\"\u003e Chiyuri\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc4.webp?91a4fd1b\" alt=\"Kotohime\" width=\"24\" height=\"24\"\u003e Kotohime\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc8.webp?a83a4201\" alt=\"Yumemi\" width=\"24\" height=\"24\"\u003e Yumemi\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc6.webp?bfdfae8f\" alt=\"Rikako\" width=\"24\" height=\"24\"\u003e Rikako\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe opponents for the first 6 stages, however, are indeed completely random, and picked by master.lib's reimplementation of the Borland RNG. The game only needs to ensure that no character is picked twice, which it does like this:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003econst int stage_7_opponent = HARDCODED_STAGE_7_OPPONENT_FOR[playchar];\nbool opponent_seen[7] = { false };\n\nfor(int stage = 0; stage \u0026lt; 6; stage++) {\n\tint candidate;\n\tdo {\n\t\t// Pick a random character between Reimu and Rikako\n\t\tcandidate = (irand() % 7);\n\t} while(opponent_seen[candidate] || (stage_7_opponent == candidate));\n\topponent_seen[candidate] = true;\n\tstory_opponent[stage] = candidate;\n}\u003c/pre\u003e\u003cfigcaption\u003e\n\tCharacters are numbered from 0 (\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu) to 8 (\u003cimg src=\"/static/emoji-th03-pc8.webp?a83a4201\" alt=\"Yumemi\" width=\"24\" height=\"24\"\u003e Yumemi), following the order in the Stage 7 table above.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tYup. For every stage, ZUN re-rolls until the RNG returns a character who hasn't yet been seen in a previous stage – even in Stage 6 where there's only one possible character left. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Since each successive stage makes it harder for the inner loop to find a valid answer, you start to wonder if there is some unlucky combination of seed and player character that causes the game to just hang forever.\u003cbr\u003e\n\tSo I tested all possible 2\u003csup\u003e32\u003c/sup\u003e seed values for all 9 player characters and… nope, \u003ca href=\"https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use\"\u003eBorland's RNG\u003c/a\u003e is good enough to eventually always return the only possible answer. The inner loop for Stage 6 \u003ci\u003edoes\u003c/i\u003e occasionally run for a disproportionate number of iterations, with the worst case being 134 re-rolls when playing \u003cimg src=\"/static/emoji-th03-pc6.webp?bfdfae8f\" alt=\"Rikako\" width=\"24\" height=\"24\"\u003e Rikako's Story Mode with a seed value of \u003ccode\u003e0x099BDA86\u003c/code\u003e. But even that is many orders of magnitude away from manifesting as any kind of noticeable delay. And on average, it just takes 17.15 iterations to determine all 6 random opponents.\n\u003c/p\u003e\u003chr id=\"demo-2025-05-10\"\u003e\u003cp\u003e\n\tThe attract demos are another intriguing aspect that I initially didn't even have on my radar for the main menu. \u003ca href=\"https://touhou-memories.com/post/779125694309564416\"\u003etouhou-memories raises an interesting question\u003c/a\u003e: The demos start at Gauge and Boss Attack level 9, which would imply Lunatic difficulty, but the enemy formations don't match what you'd normally get on Lunatic. So, which difficulty were they recorded on?\u003cbr\u003e\n\tOur already RE'd code clears up the first part of that question. TH03's demos are not recordings, but simply regular VS rounds in \u003ci\u003eCPU vs. CPU\u003c/i\u003e mode that automatically quit back to the title screen after 7,000 frames. They can only possibly appear pre-recorded because the game cycles through a mere four hardcoded character pairings with fixed RNG seeds:\n\u003c/p\u003e\u003cfigure class=\"demo-2025-05-10\"\u003e\u003ctable style=\"text-align: left;\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\u003cth\u003eDemo #\u003c/th\u003e\u003cth\u003eP1\u003c/th\u003e\u003cth\u003eP2\u003c/th\u003e\u003cth\u003eSeed\u003c/th\u003e\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\u003cth\u003e1\u003c/th\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc1.webp?c3a345a5\" alt=\"Mima\" width=\"24\" height=\"24\"\u003e Mima\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc0.webp?a2132d31\" alt=\"Reimu\" width=\"24\" height=\"24\"\u003e Reimu\u003c/td\u003e\u003ctd\u003e600\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e2\u003c/th\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc2.webp?18385020\" alt=\"Marisa\" width=\"24\" height=\"24\"\u003e Marisa\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc6.webp?bfdfae8f\" alt=\"Rikako\" width=\"24\" height=\"24\"\u003e Rikako\u003c/td\u003e\u003ctd\u003e1000\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e3\u003c/th\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc3.webp?9101ba3a\" alt=\"Ellen\" width=\"24\" height=\"24\"\u003e Ellen\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc5.webp?cba094a2\" alt=\"Kana\" width=\"24\" height=\"24\"\u003e Kana\u003c/td\u003e\u003ctd\u003e3200\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e4\u003c/th\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc4.webp?91a4fd1b\" alt=\"Kotohime\" width=\"24\" height=\"24\"\u003e Kotohime\u003c/td\u003e\u003ctd\u003e\u003cimg src=\"/static/emoji-th03-pc2.webp?18385020\" alt=\"Marisa\" width=\"24\" height=\"24\"\u003e Marisa\u003c/td\u003e\u003ctd\u003e500\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tCertainly an odd choice if your game already had the feature to let arbitrary CPU-controlled characters fight each other. That would have even naturally worked for the trial version, which doesn't contain demos at all.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tThen again, even a \"random\" character selection would have appeared deterministic to an outside observer. As usual for PC-98 Touhou, the RNG seed is initialized to 0 at startup and then simply increments after every frame you spend on the title screen and inside the top-level main, Option, and character selection menus – and yes, it does stay constant inside the VS Start menu. But since these demos always start after waiting exactly 520 frames on the title screen \u003ci\u003ewithout\u003c/i\u003e pressing any key to enter the main menu, there's no actual source of randomness anywhere. ZUN could have classically initialized the RNG with the current system time, which is what we used to do back in the day before operating systems had easily accessible APIs for true randomness, but he chose not to, for whatever reason.\n\u003c/p\u003e\u003cp\u003e\n\tThe difficulty question, however, is not so easy to answer. The \u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6d836b3a3142543830b519792f11938030863e4/th03/op_01.cpp#L393-L413\"\u003edemo startup code in the main menu doesn't override the configured difficulty\u003c/a\u003e, and neither does any other of the binaries depending on the demo ID. This \u003ci\u003eseems\u003c/i\u003e to suggest that the demos simply run at the difficulty you last configured in the Option menu, just like regular VS matches. But then, you'd expect them to run differently depending on that difficulty, which they demonstrably don't. They \u003ci\u003ealways\u003c/i\u003e start on Gauge and Boss Attack level 9, and their last frame before the exit animation is always identical, right down to the score, reinforcing the pre-recorded impression:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 640px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Demo-1-final-frame.webp?2e345fd2\"\n\t\tclass=\"active\"\n\t\tdata-title=\"Mima vs. Reimu\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the last frame (#7,000) of TH03's first demo (Mima vs. Reimu).\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Demo-2-final-frame.webp?e7596a0d\"\n\t\tdata-title=\"Marisa vs. Rikako\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the last frame (#7,000) of TH03's second demo (Marisa vs. Rikako).\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Demo-3-final-frame.webp?a36fab75\"\n\t\tdata-title=\"Ellen vs. Kana\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the last frame (#7,000) of TH03's third demo (Ellen vs. Kana).\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Demo-4-final-frame.webp?d1a3b1ee\"\n\t\tdata-title=\"Kotohime vs. Marisa\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of the last frame (#7,000) of TH03's fourth demo (Kotohime vs. Marisa).\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tNote that it takes much longer than the expected 2:04 minutes for the game to reach this end state. Each \u003ci\u003eWARNING!! You are forced to evade / Your life is in peril\u003c/i\u003e popup freezes gameplay for 26 frames which don't count toward the demo frame counter. That's why these popups will provide such a great \u003ca href=\"/blog/2024-04-24#netplay-2024-04-24\"\u003e📝 resynchronization opportunity for netplay\u003c/a\u003e. It's almost as if Versus Touhou was designed from the start with rollback netcode in mind! \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"bal-2025-05-10\"\u003e\u003cp\u003e\n\tWith quite a bit of time left over in the second push, it made sense to look at a bit of code around the Gauge and Boss Attack levels to hopefully get a better idea of what's going on there. The Gauge Attack levels are very straightforward – they can range from 1 to 16 inclusive, which matches the range that the game can depict with its gaiji, and all parts of the game agree about how they're interpreted:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2025-05-10-TH03-GAMEFT.BFT-proportional-digits.webp?af10d506\" width=\"512\" alt=\"The 16 proportional digit gaiji from TH03's GAMEFT.BFT\"\u003e\n\t\u003cfigcaption\u003e\n\t\tStored in \u003ccode\u003eGAMEFT.BFT\u003c/code\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe same can't be said about the Boss Attack level though, as the gauge and the \u003ci\u003eWARNING!!\u003c/i\u003e popup interpret the same internal variable as two different levels?\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Boss-Attack-level-9.webp?c1e7d9c6\"\n\t\twidth=\"640\"\n\t\talt=\"Darkened screenshot of a TH03 Boss Attack fired off near the beginning of a match at Lunatic difficulty, highlighting the discrepancy between the Boss Attack level shown in the gauge at the bottom of each playfield (10) and the one shown in the WARNING!! popup (9)\"\n\t\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis apparent inconsistency raises quite a few questions. After all, these gaiji have to be addressed by adding an offset from 0 to 15 to the ID of the \u003cstrong\u003e1\u003c/strong\u003e gaiji, but the levels are supposed to range from 1 to 16. Does this mean that one of these two displays has an off-by-one error? You can't fire a Level 0 Boss Attack because the level always increments before every attack, but would 0 still be a technically valid Boss Attack level?\u003cbr\u003e\n\tDecompiling the static HUD code debunks at least the first question as ZUN resolves the apparent off-by-one error by explicitly capping the displayed level to 16. And indeed, if a round lasts until the maximum Boss Attack level, the two numbers end up matching:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2025-05-10-TH03-Boss-Attack-level-16.webp?1f21fd1f\"\n\t\twidth=\"640\"\n\t\talt=\"Darkened screenshot of a TH03 Boss Attack fired off near the end of a match, highlighting how both the gauge and the WARNING!! popup agree on the level once it reaches 16\"\n\t\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis suggests that the popup indicates the level of the \u003ci\u003eincoming\u003c/i\u003e attack while the gauge indicates the level of the \u003ci\u003enext\u003c/i\u003e one to be fired by any player. That said, this theory not only needs tons of comments to explain it within the code, but also contradicts \u003ca href=\"https://en.touhouwiki.net/wiki/Phantasmagoria_of_Dim.Dream/Translation/Manual\"\u003e\u003ccode\u003e夢時空.TXT\u003c/code\u003e\u003c/a\u003e, which explicitly describes the level next to the gauge as the \u003cspan lang=\"ja\"\u003e\u003cq class=\"hovertext\" title=\"current boss attack level\"\u003e現在のＢＯＳＳアタックのレベル\u003c/q\u003e\u003c/span\u003e. Still, it remains our best bet until we've decompiled a few of the Boss Attacks and saw how they actually use this single variable.\n\u003c/p\u003e\u003chr id=\"tuning-2025-05-10\"\u003e\u003cp\u003e\n\tSo, what does this tell us about the demo difficulty? Now that we can search the code for these variables, we quickly come across \u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6d836b3a3142543830b519792f11938030863e4/th03_main.asm#L727-L731\"\u003ethe dedicated demo-specific branch that initializes these levels to the observable fixed values\u003c/a\u003e, along with two other variables I haven't researched so far. This confirms that demos run at a custom difficulty, as the two other variables receive slightly different values in regular gameplay.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, it's still a good idea to check the code for any other potential effects of the difficulty setting. Maybe they're just hard to spot in demos? Doesn't difficulty typically affect a whole lot of other things in Touhou game code? Well, not in TH03 – \u003ccode\u003eMAIN.EXE\u003c/code\u003e only ever looks at the configured difficulty in three places, and all of them are part of the code that initializes a new round.\u003cbr\u003e\n\tThis reveals the true nature of difficulty in TH03: It's \u003ci\u003eexclusively\u003c/i\u003e specified in terms of these \u003cspan class=\"hovertext\" title=\"Two for the per-player Gauge Attack level, one for the single shared Boss Attack level, and the two unknown ones.\"\u003efive variables\u003c/span\u003e, and the Easy/Normal/Hard/Lunatic/\"Demo\" settings can be thought of as simply being presets for them. Story Mode adds \u003ca href=\"/blog/2020-01-29\"\u003e📝 the AI's number of safety frames\u003c/a\u003e to the list of variables and factors the current stage number into their values, but the concept stays the same. In this regard, TH03's design is unusually clean, making it perhaps the only Touhou game with not even a single \u003ci\u003e\"if difficulty is this, then do that\"\u003c/i\u003e branch in script code. It's certainly the only \u003ci\u003ePC-98\u003c/i\u003e Touhou game with this property.\n\u003c/p\u003e\u003cp\u003e\n\tBut it gets even better if we consider what this means for netplay. We now know that the configured difficulty is part of the match-defining parameters that must be synced between both players, just like the selected characters and the RNG seed. But why stop there? How about letting players not just choose between the presets, but allowing them to customize each of the five variables independently? Boom, we've just skyrocketed the replay value of netplay. 🚀 It's discoveries like these that justify my decision to start the road toward netplay by decompiling all of \u003ccode\u003eOP.EXE\u003c/code\u003e: In-engine menus are the cleanest and most friendly way of allowing players to configure all these variables, and now they're also the easiest and most natural choice from a technical point of view.\n\u003c/p\u003e\u003chr id=\"th02-pi-2025-05-10\"\u003e\u003cp\u003e\n\tBut wait, there's still some time left in that second push! The remaining fraction of the \u003ccode\u003eOP.EXE\u003c/code\u003e reverse-engineering contribution had repeating decimals, so let's do some quick TH02 PI work to remove the matching instance of repeating decimals from the backlog. This was very much a continuation of \u003ca href=\"/blog/2024-02-03#th02-pi-2024-02-03\"\u003e📝 last year's light PI work\u003c/a\u003e; while the regular TH02 decompilation progress has focused and will continue to focus on the big features, it still left plenty of low-hanging PI fruit in boss code.\u003cbr\u003e\n\t\u003cspan style=\"\n\t\tdisplay: grid;\n\t\tgrid-template-columns: max-content 1fr max-content;\n\t\talign-items: center;\n\t\tgap: 0.5em;\n\t\"\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH02-Stage-3-midboss.webp?4cb2c1b3\"\n\t\t\talt=\"The first animation frame of TH02's Stage 3 midboss, taken from STAGE2.BMT\"\n\t\t\u003e\n\t\t\u003cspan\u003eBack then, we left with the positions of the Five Magic Stones, where ZUN's choice of storing them in arrays was almost revolutionary compared to what we saw in TH01. The same now applies to the state flags and total damage amount of not just the boss of Stage 3, but also the two independently damageable entities of the stage's midboss. In total, all of the newly identified arrays made up 3.36% of all memory references in TH02, and we're not even \u003ci\u003edone\u003c/i\u003e with Stage 3.\u003c/span\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2025-05-10-TH02-Stage-3-midboss.webp?4cb2c1b3\"\n\t\t\talt=\"The first animation frame of TH02's Stage 3 midboss, taken from STAGE2.BMT\"\n\t\t\u003e\n\t\u003c/span\u003e\n\u003c/p\u003e\u003chr id=\"ports-2025-05-10\"\u003e\u003cp\u003e\n\tActually, you know what, let's round out that second push with even more low-hanging PI fruit and ensure \u003ca href=\"/blog/2019-12-28\"\u003e📝 technical position independence\u003c/a\u003e for TH03's \u003ccode\u003eMAINL.EXE\u003c/code\u003e. This was very helpful considering that I'm going to build netplay into the \u003ccode\u003eanniversary\u003c/code\u003e branch, whose \u003ccode\u003edebloated\u003c/code\u003e foundation \u003ca href=\"/blog/2023-03-05#single-2023-03-05\"\u003e📝 aims to merge every game into as few executables as possible\u003c/a\u003e. Due to TH03's overall lower level of bloat and the dedicated SPRITE16-based rendering code in \u003ccode\u003eMAIN.EXE\u003c/code\u003e, it might not make \u003ci\u003eas\u003c/i\u003e much sense to merge all three of TH03's .EXE binaries as it did for TH01, and \u003ccode\u003eMAIN.EXE\u003c/code\u003e's lack of position independence currently prevents this anyway. However, merging just \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINL.EXE\u003c/code\u003e makes tremendous sense not just for TH03, but for the other three games as well. These binaries have a much smaller ratio of ZUN code to library code, and use the same file formats and subsystems.\u003cbr\u003e\n\tBut that's not even the best part! Once we've factored out all the invisible inconsistencies between the games, we get to share \u003ci\u003eall\u003c/i\u003e of this code across \u003ci\u003eall\u003c/i\u003e of the four games. Hence, technical position independence for TH03's \u003ccode\u003eMAINL.EXE\u003c/code\u003e also was the final obstacle in the way of a single consistent and ultimately portable version of all of this code. 🙌\n\u003c/p\u003e\u003cp\u003e\n\tSo, how do we go from here to \u003ca href=\"/blog/2024-04-24#compipes-2024-04-24\"\u003e📝 the short-term half-PC-98/half-modern netplay option\u003c/a\u003e that Ember2528 is now funding? Most of the netcode will be unrelated to TH03 in particular, but we'd obviously still want to reverse-engineer more of \u003ccode\u003eMAIN.EXE\u003c/code\u003e to ensure a high-quality integration. So how about alternating the upcoming deliveries between pure RE work and any new or modded code? Next up, therefore, I'll go for the latter and debloat \u003ccode\u003eOP.EXE\u003c/code\u003e so that I can later add the netplay features without pulling my hair out. At that point, it also makes sense to take the first steps into portability; I've got some initial ideas I'm excited to implement, and Congrio's tiny bit of funding just begs to be removed from the backlog. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\t(And I'm definitely going to defuse all the tearing landmines because my goodness are they infuriating when slowing down the game or working with screen recordings.)\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-05-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-04-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-05-10T20:41:57Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-04-25",
      "url": "https://rec98.nmlgc.net/blog/2025-04-25",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-05-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-04-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-04-25\"\u003e\u003ctime datetime=\"2025-04-25T23:37:38Z\"\u003e2025-04-25 23:37\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0310\"\u003eP0310\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (SDL Windows 98 backport, part 1 / D3DWindower support)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0309...P0310\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eRoot, \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\r\n\tBackporting my Shuusou Gyoku build to Windows 98 was one of my favorite commissions in recent history. If you remember \u003ca href=\"/blog/2024-07-09#tiers-2024-07-09\"\u003e📝 last year's backport of the overhauled ReC98 build system to Windows 9x\u003c/a\u003e, it left me rather demoralized at the end of it all. Sure, it may be the technically fastest way of fully rebuilding the entire codebase, but it just doesn't matter to me personally – incremental rebuilds on modern systems are still faster and much better integrated with the editors I actually use. People might have appreciated the research that went into it, as usual, but it just feels so pointless if nobody actually uses the result. So why are we treating Windows 9x compatibility as this noble goal and ideal expectation again? Just because retro-computing communities exist and prefer to paint it that way? The length of this post should hopefully make it clear that this is nothing that should be demanded or taken for granted.\u003cbr\u003e\r\n\tThat's why seeing this goal in particular getting funded was such a refreshing change of perspective. Finally, retro-computing people have put their money where their mouth is, and invested in something other than hardware! 🙌\r\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#strategy-2025-04-25\"\u003eThe backporting strategy\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#9xcompat-2025-04-25\"\u003eA compatibility layer for Microsoft's C runtime\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#zig-2025-04-25\"\u003eA brief look back at Zig\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl-2025-04-25\"\u003eCompiling SDL 2 for Windows 98\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#d3dwindower-2025-04-25\"\u003eRestoring compatibility with D3DWindower\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sjis-2025-04-25\"\u003eHandling Shift-JIS and Unicode\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#sjis-text-2025-04-25\"\u003eText rendering\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sjis-fns-2025-04-25\"\u003eThe original Japanese filenames\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sjis-unicows-2025-04-25\"\u003eLeaving it all to \u003ccode\u003eunicows.dll\u003c/code\u003e?\u003c/a\u003e \u003c/li\u003e\u003c/ul\u003e\u003c/ol\u003e\u003chr id=\"strategy-2025-04-25\"\u003e\u003cp\u003e\r\n\tSo, how \u003ci\u003edo\u003c/i\u003e you backport a modern C++ project to Windows 98 in 2025? Visual Studio removed official support for such old systems a long time ago, and increasingly uses newfangled Win32 API functions in its C++ standard library implementations where they can't be trivially removed.\u003cbr\u003e\r\n\tIf your codebase of choice restricts itself to old C and C++ standards, compiling it with an old version of Visual Studio can get you most of the way there. But this is becoming increasingly unlikely as we only ever move further away from the mid-90s. After all, this restriction would not only have to apply to a project's own code, but to all of its dependencies as well, since a backport can't just fall back on precompiled libraries. And then, all bets are off – some projects like miniaudio \u003ca href=\"https://github.com/mackron/miniaudio/pull/140\"\u003emight be committed to supporting Visual C++ 6\u003c/a\u003e, but others might just freely use whatever language features are available on the GCC version that is part of the oldest Linux image offered by their CI provider. Which is totally understandable: \u003ca href=\"https://www.youtube.com/watch?v=wrwwa68JXNk\"\u003eThere is a reason behind new language versions\u003c/a\u003e, and at some point, developers just want to move on and stop taking productivity hits all the time. Or just prefer to try something new, because C89 in particular sure gets old after writing a 5-digit number of lines in it, at least as far as I'm concerned. I'm still hoping that I get to statically recompile Turbo C++ 4.0J one day and add at least a few more language features and code optimizations to it…\u003cbr\u003e\r\n\tAlso, having simple and accessible build processes has always been a guiding principle of mine. If people can't compile with widely available tools and have to acquire old proprietary compilers from legally dubious sources, I don't fully deliver on a key promise of free software, which is kind of important to me.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tBut as long as the Windows 98 users are willing to install KernelEx, we can get very far with even current Visual Studio versions. KernelEx covers most of those newfangled Win32 API functions, and even helpfully makes Windows ignore the \u003ccode\u003e*OperatingSystemVersion\u003c/code\u003e fields in the PE header. The only thing we should manually add to the build process is the \u003ccode\u003e/arch:IA32\u003c/code\u003e flag: It removes any modern x86 instructions in newly-compiled code and thus ensures that the game still runs on period-correct CPUs. Of course, \u003ca href=\"https://github.com/nmlgc/ssg/issues/84\"\u003ethe modern build should use all modern instructions it possibly can\u003c/a\u003e, but it makes sense to limit Windows 98 support to the alternate build with pbg's original DirectDraw and Direct3D graphics and add the flag there.\u003cbr\u003e\r\n\tAnd sure enough, \u003ca href=\"/blog/2023-08-01#strategy-2023-08-01\"\u003e📝 this worked out beautifully for the first few releases of my Shuusou Gyoku fork\u003c/a\u003e. But once I added more features, running on Windows 98 became increasingly harder:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ssg/releases/tag/P0256\"\u003eP0256\u003c/a\u003e required an extra \u003ca href=\"https://learn.microsoft.com/en-us/cpp/build/reference/zc-threadsafeinit-thread-safe-local-static-initialization\"\u003e\u003ccode\u003e/Zc:threadSafeInit-\u003c/code\u003e\u003c/a\u003e to not use certain Win32 lock functions that KernelEx doesn't cover.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ssg/releases/tag/P0275\"\u003eP0275\u003c/a\u003e then started using the filesystem and thread features from modern C++, whose Microsoft STL implementations used enough unimplemented Win32 functions that I was forced to drop Windows 98 support for the time being. It sure doesn't help that KernelEx development has never escaped the increasingly locked-down forum it started in, which has made new builds increasingly inaccessible.\u003c/li\u003e\r\n\t\u003cli\u003eMeanwhile, Microsoft's C runtime had started to steadily remove more and more workarounds that were required to run on Windows 9x, after they've probably annoyed the developers for long enough.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tSo let's finally give this backport the dedicated attention it needs, and start the usual backporting loop:\r\n\u003c/p\u003e\u003col\u003e\r\n\t\u003cli\u003eEncounter one of the classic DLL function errors at startup\u003c/li\u003e\r\n\t\u003cli\u003eLook at the disassembly to figure out where that call came from\u003c/li\u003e\r\n\t\u003cli\u003eEither rewrite the offending code to not use the function, or find some way of polyfilling it if the call originated from code that is not under your direct control\u003c/li\u003e\r\n\t\u003cli\u003eRepeat until the game works\u003c/li\u003e\r\n\t\u003cli\u003eFollow the same steps for any crashes or weird behavior introduced by the older Windows version\u003c/li\u003e\r\n\u003c/ol\u003e\u003cp\u003e\r\n\tThere is some room for creativity in this process, as well as non-zero hack value and enjoyment from seeing it all work out in the end. Heck, \u003ca href=\"https://www.youtube.com/watch?v=CTUMNtKQLl8\"\u003eMattKC even made a blockbuster feature film out of it\u003c/a\u003e. But ultimately, it's dumb drudge work that wouldn't be worth doing if no actual person cares.\u003cbr\u003e\r\n\tAnd I haven't even mentioned the worst part: Setting up a full-featured, bug-free, and performant VM that connects to your development system in a sort of comfortable way – and then repeating this process for different language versions of Windows 98, and even for Windows XP and maybe 7 when it comes to debugging DirectDraw issues. This only gets harder as the required dedicated VM code for these old systems starts to bit-rot, which left apparently every VM software out there with at least one deprecated or already removed feature…\r\n\u003c/p\u003e\u003chr id=\"9xcompat-2025-04-25\"\u003e\u003cp\u003e\r\n\tSo, let's start by looking at the Win32 API functions referenced by Microsoft's C runtime, and \u003ca href=\"https://github.com/nmlgc/9xcompat\"\u003ecreate a separate project for any required polyfills\u003c/a\u003e so that they would never annoy anyone. I only found out later that \u003ca href=\"https://building.enlyze.com/posts/modern-visual-studio-meets-ancient-windows/\"\u003esomeone else had the same idea\u003c/a\u003e, but \u003ca href=\"https://building.enlyze.com/posts/targeting-25-years-of-windows-with-visual-studio-2019/\"\u003equickly moved on to targeting Clang due to hitting a roadblock with \u003ccode\u003estd::mutex\u003c/code\u003e\u003c/a\u003e and \u003ca href=\"https://github.com/enlyze/EnlyzeWinCompatLib/issues/1\"\u003eonly ever wanted to target NT kernels anyway\u003c/a\u003e. So there definitely was a point to me starting a new project there. It also gave me the chance to approach the whole \u003ca href=\"https://building.enlyze.com/posts/modern-visual-studio-meets-ancient-windows/#overwriting-dll-imports\"\u003eproblem of overwriting and redirecting DLL imports\u003c/a\u003e from multiple angles, which led me to arrive at two slightly different solutions. Check the project's README for more details on that.\u003cbr\u003e\r\n\tAfter I previously \u003ca href=\"/blog/2025-04-09#sdl3-2025-04-09\"\u003e📝 migrated all C++ threading code to SDL as part of the Linux port\u003c/a\u003e, I only needed to polyfill a total of 8 functions to get the game back running:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/fibersapi/\"\u003eThe 4 fiber-local storage API functions\u003c/a\u003e (\u003ccode\u003eFls*()\u003c/code\u003e), which we redirect to the corresponding \u003ccode\u003eTls*()\u003c/code\u003e functions just like Microsoft did in earlier Windows SDK versions.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getlocaleinfoex\"\u003e\u003ccode\u003eGetLocaleInfoEx()\u003c/code\u003e\u003c/a\u003e, which is used by \u003ccode\u003estd::filesystem\u003c/code\u003e's error message implementation. We currently don't use these messages, but the retrieval method still gets linked into the binary because the respective method is part of a vtable.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-initializecriticalsectionex\"\u003e\u003ccode\u003eInitializeCriticalSectionEx()\u003c/code\u003e\u003c/a\u003e, which bumps the minimum OS requirement to Vista for no reason because the CRT only ever passes \u003ccode\u003e0\u003c/code\u003e to the \u003ccode\u003eFlags\u003c/code\u003e parameter. Easily redirected to the older \u003ccode\u003eInitializeCriticalSectionAndSpinCount()\u003c/code\u003e.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getlocaleinfoex\"\u003e\u003ccode\u003eGetFileInformationByHandleEx()\u003c/code\u003e\u003c/a\u003e, used by \u003ccode\u003estd::filesystem\u003c/code\u003e's directory iterator. This was the only specific function needed to get BGM modding working.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfileexw\"\u003e\u003ccode\u003eFindFirstFileExW()\u003c/code\u003e\u003c/a\u003e, which is also used by, you guessed it, \u003ccode\u003estd::filesystem\u003c/code\u003e. The CRT uses the \u003ca href=\"https://www.wholetomato.com/blog/2024/11/14/how-to-query-file-attributes-50x-faster-on-windows/\"\u003esometimes faster\u003c/a\u003e \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ne-minwinbase-findex_info_levels\"\u003e\u003ccode\u003eFindExInfoBasic\u003c/code\u003e mode introduced in Windows 7\u003c/a\u003e, which we simply replace with \u003ccode\u003eFindExInfoStandard\u003c/code\u003e. For now, we leave the Unicode→ANSI wrapping to the KernelEx-injected \u003ccode\u003eunicows.dll\u003c/code\u003e from the \u003ca href=\"https://learn.microsoft.com/en-us/archive/msdn-magazine/2001/october/mslu-develop-unicode-applications-for-windows-9x-platforms-with-the-microsoft-layer-for-unicode\"\u003eMicrosoft Layer for Unicode\u003c/a\u003e.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tNot only were these functions enough to cover Windows 98 with \u003ca href=\"https://github.com/nmlgc/ssg/issues/43\"\u003ethe one version of KernelEx I managed to snag from the MSFN forum before they disabled downloads\u003c/a\u003e, but they also made the game run on unmodified Windows XP again! To completely remove the need for KernelEx and \u003ccode\u003eunicows.dll\u003c/code\u003e, we'd still have to cover a slightly bigger number of Win32 API functions, though. But now that this push has put all the foundations into place, the chances are good that the next push might already get this done. And at that point, even \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/43\"\u003eWindows 95 support\u003c/a\u003e wouldn't be far away.\u003cbr\u003e\r\n\tIf it takes longer, it'll probably be due to these two other remaining issues:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eThe game currently crashes every time it's closed on Windows 9x. We can thank the Microsoft Store for that one: Store apps have to be terminated differently than regular Windows programs, but Visual Studio only uses a single (or, as they call it, \u003cq\u003euniversal\u003c/q\u003e) C runtime for both kinds of programs. As a result, the CRT has to reach deep into the \u003ca href=\"https://en.wikipedia.org/wiki/Process_Environment_Block\"\u003eNT Process Environment Block\u003c/a\u003e to find out what kind of program it's dealing with, and this structure simply doesn't exist in Windows 9x.\r\n\t\u003cfigure\u003e\u003cpre\u003eextern \"C\" bool __cdecl __acrt_is_secure_process()\r\n{\r\n    return (NtCurrentTeb()-\u003eProcessEnvironmentBlock-\u003eProcessParameters-\u003eFlags \u0026 RTL_USER_PROC_SECURE_PROCESS) != 0;\r\n}\r\n\u003c/pre\u003e\u003cfigcaption\u003eTaken from \u003ccode\u003eucrt/internal/peb_access.cpp\u003c/code\u003e.\u003c/figcaption\u003e\u003c/figure\u003e\r\n\tWorking around this one will be slightly more difficult than just polyfilling a single function.\u003c/li\u003e\r\n\t\u003cli\u003eCurrent Windows SDK versions have started to use SSE instructions in the \u003ca href=\"https://learn.microsoft.com/en-us/cpp/build/reference/gs-buffer-security-check?view=msvc-170\"\u003einitialization code for the buffer security cookie\u003c/a\u003e. Requiring SSE may look like a non-issue nowadays, but shuts out a significant chunk of late-90s systems that would otherwise run the game without any issues. \u003ca href=\"https://twitter.com/Columbio184/status/1914164260076139002/photo/1\"\u003eThis laptop from 1999 with an AMD K6-2 CPU\u003c/a\u003e, for example, can no longer run the new build, even though \u003ca href=\"https://twitter.com/Columbio184/status/1685823332300562433\"\u003eit flawlessly ran my fork two years ago\u003c/a\u003e.\u003c/li\u003e\r\n\u003c/ul\u003e\u003chr id=\"zig-2025-04-25\"\u003e\u003cp\u003e\r\n\tThe second issue in particular shows the limits of this approach. It's only a matter of time until Microsoft activates unconditional SSE on every single part of the precompiled CRT, forcing us to reimplement pretty much all of it for continued 9x compatibility.\u003cbr\u003e\r\n\tThis is exactly why I prefer the Zig approach of compiling the C standard library on demand against the chosen CPU model. Looking at Zig's recent progress, I'm very impressed to see that the community has addressed almost \u003ca href=\"/blog/2023-09-30#zig-2023-09-30\"\u003e📝 all of my pain points\u003c/a\u003e in the 1½ years since I last looked at it. The Zig compiler now has \u003ca href=\"https://github.com/ziglang/zig/pull/18704\"\u003ePDB basenames\u003c/a\u003e, \u003ca href=\"https://github.com/ziglang/zig/pull/20059\"\u003ecompilation progress output\u003c/a\u003e, \u003ca href=\"https://ziglang.org/devlog/2025/#2025-02-24\"\u003eimproved UBSan error messages\u003c/a\u003e, and \u003ca href=\"https://ziglang.org/download/0.14.0/release-notes.html#x86-Backend\"\u003ecompilation speed is actively being worked on\u003c/a\u003e.\u003cbr\u003e\r\n\tUnfortunately though, they still \u003ca href=\"https://ziglang.org/download/0.14.0/release-notes.html#Build-System\"\u003ebreak the build system all the time\u003c/a\u003e. For a system-level dependency that people can and will use different versions of, that kind of instability is a non-starter. So I'm very likely not going to migrate anything to Zig before the compiler hits 1.0, unless they do a feature freeze or make some other kind of compatibility promise before that point. Oh well, I've put too much effort into \u003ca href=\"https://github.com/nmlgc/tupblocks\"\u003emy Tup building blocks\u003c/a\u003e to not continue using them for at least a few more years.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tIt might seem like \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/80\"\u003ecompiling with MinGW\u003c/a\u003e would be the more reliable alternative here. Even its GCC 14 version still sets the \u003ccode\u003e*OperatingSystemVersion\u003c/code\u003e and \u003ccode\u003e*SubsystemVersion\u003c/code\u003e fields to 4.0, indicating Windows 95, when compiling a 32-bit binary. And if MinGW ever decides on a higher default, I'm sure that the \u003ccode\u003e--(major|minor)-(os|subsystem)-version\u003c/code\u003e linker flags will continue to allow that default to be freely overridden. Unlike Visual Studio 2022's \u003ccode\u003eLINK\u003c/code\u003e and \u003ccode\u003eEDITBIN\u003c/code\u003e tools, which refuse OS version 4.0 for no particular reason. 🙄\u003cbr\u003e\r\n\tHowever, MinGW is hardcoded to link against the DLL version of Microsoft's C runtime and offers no option of statically linking the CRT, \u003ca href=\"https://sourceforge.net/p/mingw-w64/discussion/723798/thread/d0f3119ec6/\"\u003epresumably due to legal reasons\u003c/a\u003e. This used to be no problem as its GCC ≤13 versions linked against the generic \u003ccode\u003emsvcrt.dll\u003c/code\u003e, which is available on Windows 98 as well. But this was bad for \u003ca href=\"https://devblogs.microsoft.com/oldnewthing/20140411-00/?p=1273\"\u003emultiple\u003c/a\u003e \u003ca href=\"https://sourceforge.net/p/mingw-w64/wiki2/The%20case%20against%20msvcrt.dll/\"\u003ereasons\u003c/a\u003e. And so, even \u003ci\u003ethey\u003c/i\u003e ultimately decided that Windows 7 was a reasonable minimum requirement these days and made MinGW's GCC 14 version link against the Universal CRT, with all its \u003ccode\u003eapi-ms-win-crt-*-l1-1-0\u003c/code\u003e DLLs. We can only avoid these DLL dependencies by going \u003ccode\u003e-nostdlib\u003c/code\u003e and rewriting all our code accordingly – but guess what, we could do the exact same thing on MSVC with \u003ccode\u003e/NODEFAULTLIB\u003c/code\u003e, without switching compilers.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tUnfortunately, that's the same reason why Zig would make no difference, regardless of whether you use it as a compiler for C/C++ code or write pure Zig. If you build for Windows, you can merely choose between the GNU and MSVC ABI. Then, Zig behaves exactly like the respective C compiler: Select GNU and you get the UCRT dependencies, select MSVC and you get the statically linked Microsoft CRT with all of its aforementioned drawbacks. \u003ca href=\"https://github.com/ziglang/zig/issues/528\"\u003eSupposedly, it's possible to bypass MSVC\u003c/a\u003e, but the GNU ABI \u003ci\u003ewas\u003c/i\u003e the answer to the question of \u003cq\u003ecompiling without Visual Studio\u003c/q\u003e back then. Establishing an easy-to-use third ABI without any dependencies sounds like much more of a research project than just staying with C++.\u003cbr\u003e\r\n\tNot to mention that \u003ca href=\"https://ziglang.org/download/0.6.0/release-notes.html#Windows-Support\"\u003eZig's Windows version support policy follows Microsoft's extended support lifecycle\u003c/a\u003e. Zig 0.6.0 dropped Windows 7 support, and \u003ca href=\"https://ziglang.org/download/0.11.0/release-notes.html#Windows\"\u003eZig 0.11.0 dropped Windows 8.1 support\u003c/a\u003e. While \u003ca href=\"https://github.com/ziglang/zig/issues/7242#issuecomment-736772022\"\u003eAndrew Kelley is open to non-invasive patches for greater OS support\u003c/a\u003e, these could break at any moment and would therefore need consistent maintenance as well.\r\n\u003c/p\u003e\u003chr id=\"sdl-2025-04-25\"\u003e\u003cp\u003e\r\n\tSurprisingly, SDL 2 has been causing by far the least amount of problems in all of this. \u003ca href=\"https://github.com/nmlgc/SDL/commit/bf9ff7486b9ae91a10ad722ed078a09404442978\"\u003eA small adjustment to its threading functions\u003c/a\u003e removed its only mandatory reliance on Microsoft CRT code, and KernelEx and \u003ccode\u003eunicows.dll\u003c/code\u003e then cover any remaining unconditional usage of newer Win32 API functions. Since we already needed a \u003ccode\u003e__WIN9X__\u003c/code\u003e macro to opt into this change and retain SDL's default behavior on modern systems, I also took the opportunity to disable most of the subsystem backends that are unsupported on Windows 98, shaving a few hundred KB off the DLL's file size.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tThis made SDL look even better than \u003ca href=\"/blog/2025-04-09#sdl3-2025-04-09\"\u003e📝 the already good impression I got last time\u003c/a\u003e. Not only is SDL not a problem, but it's actually the biggest asset we have in a Windows 9x port. And with all the improved subsystems in SDL 3, it becomes so much of an asset that we should ideally just go all in on SDL 3 and make it a hard dependency of even the cross-platform logic code.\u003cbr\u003e\r\n\tThis would be quite a big deal, and it might not immediately be obvious why. Doesn't every one of our supported platforms already depend on SDL anyway? Internally though, my current architecture predates the plan of using SDL and is still designed for the hypothetical case of not using it. After all, \u003ca href=\"https://github.com/nmlgc/ssg/issues/53\"\u003eretaining and expanding pbg's old backend code for a slim Windows 98 port without any big dependencies was a viable option that could have been funded\u003c/a\u003e. But now that the backers have voted against it, directly architecting all code against SDL 3 would have so many upsides:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eSince we maintain our own SDL fork for Windows 9x support, we're in full control of its portability. In contrast, recompiling Microsoft's C runtime from the SDK sources shipped with Visual Studio \u003ca href=\"https://developercommunity.visualstudio.com/t/private-ucrt-from-source/113492#T-N116600\"\u003eisn't even supported anymore\u003c/a\u003e. It might still be \u003ci\u003epossible\u003c/i\u003e, but SDL is a much more easily handled and forked dependency.\u003c/li\u003e\r\n\t\u003cli\u003eSDL already did the work of \u003ca href=\"https://discourse.libsdl.org/t/31861/4\"\u003ecompletely bypassing the C standard library on Windows\u003c/a\u003e for its own code. They still use the CRT by default, but we'd simply have to undefine \u003ccode\u003eHAVE_LIBC\u003c/code\u003e and \u003ccode\u003eHAVE_STDIO\u003c/code\u003e to opt out of it.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ccode\u003e/arch:IA32\u003c/code\u003e also applies to SDL code. If we managed to completely purge all precompiled CRT code from the game binary as a result of using SDL functions wherever possible, we would have fully escaped the looming proliferation of SSE code within the CRT and ensured long-term Windows 9x compatibility.\u003c/li\u003e\r\n\t\u003cli\u003eAnd given \u003ca href=\"/blog/2023-09-30#sdl-2023-09-30\"\u003e📝 SDL's very nature as this incompressible brick of a DLL\u003c/a\u003e, it only makes sense to do so. Because I've restricted the cross-platform logic layer to the C/C++ standard library, the game binaries effectively ended up with their own implementations of features that SDL already offers. Most of those come from Microsoft's CRT, but this category also includes some makeshift code of my own that I only had to write to uphold the initial design goal. Breaking this self-imposed restriction would not only simplify the architecture, but also remove a significant amount of \u003cspan class=\"hovertext\" title=\"Which doesn't just mean file size, but also time to link a release build as part of the build process.\"\u003ebloat\u003c/span\u003e from the Windows build and even fix the occasional bug! 🤩 I've already mentioned file handling in the previous blog post, but we'd also gain \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/87\"\u003ea more sophisticated and bug-free BMP writer\u003c/a\u003e, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/23\"\u003ea standardized and configurable error logging channel\u003c/a\u003e, \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_strncasecmp\"\u003ecase-insensitive string comparison that doesn't bloat the binary with locale braindeath\u003c/a\u003e, a consistently implemented \u003ccode\u003esprintf()\u003c/code\u003e that has a defined way of printing 64-bit numbers without the ugly \u003ccode\u003ePRId64\u003c/code\u003e hack from \u003ccode\u003e\u0026lt;inttypes.h\u0026gt;\u003c/code\u003e, and probably a bunch of other things I'm missing right now. Heck, we could even \u003ca href=\"https://miniaud.io/docs/examples/engine_sdl.html\"\u003ereplace miniaudio's backends with the now sane low level of SDL\u003c/a\u003e 3's audio subsystem, limiting miniaudio's role to just mixing.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tIn fact, this idea is so convincing that it makes me want to freeze all new feature or backport development for Shuusou Gyoku until it's done. However, we absolutely want to do this with SDL 3 rather than 2 to reap the full set of benefits. This would imply removing the SDL 2 code path for good, but our Flatpak still uses this code path because the Freedesktop SDK will only start shipping SDL 3 with the next update in August. We \u003ci\u003ecould\u003c/i\u003e compile SDL 3 from source in the meantime, but maybe we shouldn't? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\r\n\tGiven the funding situation and general hype, it'll probably be best if I just focus on TH03 until then.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tBut once that's done, it would only leave TrueType fonts, MIDI, and graphics rendering as the subsystems that our architecture supports system-specific APIs for – and even MIDI will only be on there \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/75\"\u003euntil someone funds MIDI support for just a single non-Windows platform\u003c/a\u003e.\u003cbr\u003e\r\n\tYou might wonder why graphics rendering is on there, but we can unfortunately never get rid of pbg's original DirectDraw code. The 8-bit mode is just too crucial for getting the game to run decently on the old systems without 3D acceleration that a Windows 9x port is supposed to target. We could try going full GDI in the hope of maybe even being faster \u003ca href=\"https://lainnet.arcesia.net/misc/np2gdi.html\"\u003eor more portable\u003c/a\u003e, but that would just be another custom backend.\u003cbr\u003e\r\n\tWe \u003ci\u003ecould\u003c/i\u003e, however, go the opposite route. \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/88\"\u003eTurning pbg's old code into an SDL_Renderer backend\u003c/a\u003e would facilitate all kinds of backports of pure SDL_Renderer games to that late-90s period of hardware. Those games will probably not run all \u003ci\u003ethat\u003c/i\u003e well \u003ca href=\"/blog/2024-10-22#benchmark-2024-10-22\"\u003e📝 if our benchmark results for software rendering are any indication\u003c/a\u003e, but the idea definitely has hack value.\u003cbr\u003e\r\n\tAnd why stop there? Let's add a PC-98 backend! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e …yeah, I'm getting off-track.\r\n\u003c/p\u003e\u003chr id=\"d3dwindower-2025-04-25\"\u003e\u003cp\u003e\r\n\tSpeaking of pbg's old rendering path though: \u003ci\u003eHaving\u003c/i\u003e to run it while debugging the Windows 98 backport comes with the practical problem that \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/7\"\u003ewe still have no proper windowed mode for it\u003c/a\u003e. Multi-monitor support in VMs is sketchy at best, and even \u003ci\u003eif\u003c/i\u003e it works, running OllyDbg on a separate virtual monitor next to exclusive fullscreen 8-bit DirectDraw still doesn't prevent these highly disruptive mode switches between the game and debugger windows.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tFortunately, D3DWindower is old enough to still work on Windows 98 and works well with pbg's original build of Shuusou Gyoku. Unfortunately, \u003ca href=\"/blog/2023-09-30#fps-2023-09-30\"\u003e📝 it stopped working as soon as I migrated window creation to SDL 2\u003c/a\u003e. But if we put two and two together, we immediately get a theory as to why: \u003ci\u003eBecause\u003c/i\u003e it works on Windows 98, D3DWindower might only hook the ANSI versions of all the Windows API functions that games can use to enter exclusive fullscreen mode, but SDL uses the Unicode variants. You wouldn't \u003ci\u003ethink\u003c/i\u003e that a mode-switching API uses potentially localizable strings as part of its parameters, but hey, maybe monitors are treated like files and addressed with names?\u003cbr\u003e\r\n\tAnd indeed, SDL uses \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw\"\u003e\u003ccode\u003eChangeDisplaySettingsExW()\u003c/code\u003e\u003c/a\u003e, but D3DWindower only hooks \u003ccode\u003eChangeDisplaySettingsExA()\u003c/code\u003e. Switching from \u003ccode\u003eW\u003c/code\u003e to \u003ccode\u003eA\u003c/code\u003e was all it took to get it working again… on modern Windows at least. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e It wasn't enough for Windows 98, but what could we possibly be missing?\r\n\u003c/p\u003e\u003cp\u003e\r\n\tTurns out that KernelEx is the one and only issue there. D3DWindower (or rather, its internally used \u003ca href=\"https://www.madshi.net/madCodeHookDescription.htm\"\u003emadCodeHook library\u003c/a\u003e) uses \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversion\"\u003ethe Win32 \u003ccode\u003eGetVersion()\u003c/code\u003e function\u003c/a\u003e, but interchangeably calls it both via its import and via a proc address pointer retrieved directly from \u003ccode\u003ekernel32.dll\u003c/code\u003e. KernelEx only wraps one of the two, which causes the hooking algorithm to fail as it gets confused by contradictory Windows version numbers.\u003cbr\u003e\r\n\tThe problem with version numbers is that the number-returning function itself has no way of knowing the caller's intent. I can't think of a situation where it wouldn't make more sense to query the presence of a certain OS \u003ci\u003efeature\u003c/i\u003e rather than the version number of the entire thing. And so, KernelEx's wrapper makes the understandable choice of returning exactly the version you've configured for the executable:\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 374px\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-KernelEx-properties.webp?105e40a9\"\r\n\t\twidth=\"374\"\r\n\t\talt=\"Screenshot of the Windows 98 file property dialog, showing the KernelEx tab for an executable that is configured to use the specific compatbility mode for Windows 2008 SP1\"\r\n\t\u003e\u003cfigcaption\u003e\r\n\t\tThis will cause the hooked \u003ccode\u003eGetVersion()\u003c/code\u003e to return \u003ccode\u003e0x17710006\u003c/code\u003e rather than \u003ccode\u003e0xC0000A04\u003c/code\u003e.\r\n\t\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tmadCodeHook, however, uses the version number to pick between the completely different hooking strategies for 9x and NT kernels, and therefore always needs the actual version of the underlying system. Presumably, these different strategies are needed because 9x kernels didn't have the copy-on-write mechanism that allows a process to freely rewrite system DLL code without affecting other processes. Instead, \u003ca href=\"http://www.icodeguru.com/vc/ProgramingVC/ch10b.htm\"\u003e9x kernels only have a single global shared instance of all system DLLs, which gets mapped to the same address for every process\u003c/a\u003e. This is also why setting code breakpoints within system DLLs on 9x can break the entire system: Since 9x doesn't support hardware breakpoints, debuggers only have the option of writing the \u003ccode\u003eINT 3\u003c/code\u003e instruction byte (\u003ccode\u003e0xCC\u003c/code\u003e) to the breakpoint address and then reverting it before resuming execution. But this instruction can only break back into the debugger for the one process that the debugger is attached to. In the meantime, every other process is left with a corrupted instruction stream, and OllyDbg's cryptic \u003ci\u003eUnable to flush cache\u003c/i\u003e only describes a single symptom of the ensuing general instability.\u003cbr\u003e\r\n\tThankfully, KernelEx lets us disable its \u003ccode\u003eGetVersion()\u003c/code\u003e wrapper for any specific compatibility mode by editing the respective section of \u003ccode\u003e%WINDIR%\\KernelEx\\Core.ini\u003c/code\u003e. For \u003cvar\u003eWindows 2008 SP1\u003c/var\u003e, the change would be:\r\n\u003c/p\u003e\u003cfigure\u003e\r\n\t\u003cpre class=\"chroma\"\u003e [WIN2K8.names]\r\n\u003cspan class=\"gd\"\u003e-KERNEL32.GetVersion=kexbases.8\u003c/span\u003e\r\n\u003cspan class=\"gi\"\u003e+KERNEL32.GetVersion=std\u003c/span\u003e\u003c/pre\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tAfter a reboot, D3DWindower then succeeds in hooking \u003ccode\u003eChangeDisplaySettingsExA()\u003c/code\u003e through KernelEx even if the KernelEx-injected Microsoft Layer for Unicode previously redirected \u003ccode\u003eChangeDisplaySettingsExW()\u003c/code\u003e to that function.\u003cbr\u003e\r\n\tSince the ideal definition of a \u003ci\u003e\"Windows 98 backport\"\u003c/i\u003e does not include KernelEx though, it made sense to already go ANSI right now and restore general compatibility with D3DWindower on NT kernels as well. And with one more crucial manual setting that prevents SDL from crashing itself in confusion…\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 358px;\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-D3DWindower-Foreground-Control.webp?36b1c805\"\r\n\t\twidth=\"358\"\r\n\t\talt=\"Screenshot of the D3DWindower Foreground Control settings that are necessary to prevent SDL from trying to move back into fullscreen mode after the game window lost and regained focus, which will result in a weirdly stretched borderless fullscreen image followed by a crash\"\r\n\t\u003e\u003cfigcaption\u003e\r\n\t\tYes, I would have preferred a nice \u003ccode\u003eGIAN07 (Windows 98).exe\u003c/code\u003e file name, but D3DWindower unfortunately glitches if binary names contain spaces. If the directory of a hooked executable contains another executable whose name matches the hooked one up to the first space, D3DWindower will run that other executable instead of the intended one. We sure don't want to run the regular \u003ccode\u003eGIAN07.exe\u003c/code\u003e by accident.\r\n\t\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\t… we've got the game running in a provisional windowed mode on Windows 9x!\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 1059px;\"\u003e\r\n\t\u003crec98-child-switcher\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-Windows-98-screenshots.webp?a830d8d0\"\r\n\t\tclass=\"active\"\r\n\t\tdata-title=\"Screenshot menu\"\r\n\t\twidth=\"1059\"\r\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the screenshot submenu introduced in the ReC98 P0309 build, which was definitely not part of pbg's original release.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-Windows-98-BGM-pack.webp?15a46cda\"\r\n\t\tdata-title=\"BGM pack menu\"\r\n\t\twidth=\"1059\"\r\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the BGM pack selection submenu introduced in the ReC98 P0275 build, which was definitely not part of pbg's original release.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-Windows-98-Golf-course.webp?87c8de0c\"\r\n\t\tdata-title=\"D3DWindower golf course bug\"\r\n\t\twidth=\"1059\"\r\n\t\talt=\"Screenshot of the ReC98 Shuusou Gyoku build running in a 640×480 window on a Windows 98 desktop, next to the System Properties window (as usual for backport screenshots) and the D3DWindower window (demonstrating how the game was windowed). The game window shows the Stage 3 entrance animation in 8-bit mode, demonstrating how D3DWindower's inaccurate 8-bit color emulation replicates the infamous \u0026quot;golf course\u0026quot; bug.\"\r\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\r\n\t\u003cfigcaption\u003e\r\n\t\tI can't stress enough that debugging was the main intention behind this fix. Without scaling options, D3DWindower is not a replacement for a proper windowed mode, \u003ci\u003eand\u003c/i\u003e it adds its own bugs on top. \u003ca href=\"/blog/2023-08-01#palettes-2023-08-01\"\u003e📝 The P0251 blog post\u003c/a\u003e has more detail about how precise an 8-bit DirectDraw emulation has to be to avoid the infamous golf course in Stage 3.\r\n\t\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003chr id=\"sjis-2025-04-25\"\u003e\u003cp\u003e\r\n\tBut turning off Unicode in the Windows 98 build of SDL 2 also had one unfortunate drawback: The window title is now \u003ccode\u003e???\u003c/code\u003e rather than \u003ccode lang=\"ja\"\u003e秋霜玉\u003c/code\u003e, even when running on NT kernels or a Japanese version of Windows 98. That brings us right to the other big complication of this backport:\r\n\u003c/p\u003e\u003ch3\u003e😩 Handling Shift-JIS and Unicode 😩\u003c/h3\u003e\u003cp\u003e\r\n\tWith SDL now using the \u003ccode\u003e*A()\u003c/code\u003e functions, you might have expected the same mojibake that you'd see in the windowed title bar of pbg's original build. But since we pass UTF-8 to SDL rather than Shift-JIS, the result would always be slightly different. The number of question marks does match the number of codepoints in the string though, which means that SDL does convert from UTF-8 into \u003ci\u003esomething\u003c/i\u003e before passing the string to Windows. Unfortunately, this target encoding is always pure 7-bit ASCII because \u003ca href=\"https://github.com/libsdl-org/SDL/blob/90fd2a3cbee5b7ead1f0517d7cc0eba1f3059207/src/stdlib/SDL_iconv.c#L84-L100\"\u003eSDL's hand-rolled \u003ccode\u003eiconv()\u003c/code\u003e function only supports that, Latin-1, and the Unicode Transformation Formats\u003c/a\u003e.\u003cbr\u003e\r\n\tThis looks like a very bad choice on the surface. Sure, this implementation is meant to be a minimal fallback for systems that don't have \u003ca href=\"https://man7.org/linux/man-pages/man3/iconv.3.html\"\u003e\u003ccode\u003eiconv(3)\u003c/code\u003e\u003c/a\u003e, but if it uses \u003ci\u003ethat\u003c/i\u003e library when available, why doesn't it also use \u003ccode\u003eWideCharToMultiByte()\u003c/code\u003e on Windows? One reason might be right there in the name of that Win32 function: Windows treats UTF-16 as the base encoding from where all other encodings are converted, but SDL (and everyone else) prefers UTF-8 in that role. This allows SDL to directly convert to UTF-32 or Latin-1 without stopping at UTF-16 first.\u003cbr\u003e\r\n\tBut even \u003ci\u003eif\u003c/i\u003e SDL offered Win32-powered conversion from UTF-8 into any Win32 codepage, there's still the issue that \u003ccode\u003eWideCharToMultiByte(\u003cspan class=\"hovertext\" title=\"ID of the Shift-JIS codepage\"\u003e932\u003c/span\u003e)\u003c/code\u003e will most likely just not work on non-Japanese editions of Windows 9x. Since there is no algorithmic mapping between JIS and Unicode, the conversion between these two encodings requires a lookup table. Windows stores this table in \u003ccode\u003eC_932.NLS\u003c/code\u003e, and there is no guarantee that this file will be installed on anything before Vista.\r\n\u003c/p\u003e\u003cp id=\"sjis-text-2025-04-25\"\u003e\r\n\tOn the other hand, the second screenshot above clearly shows that…\r\n\u003c/p\u003e\u003ch4\u003eText rendering\u003c/h4\u003e\u003cp\u003e\r\n\t…just works for Japanese text? On \u003ci\u003emy\u003c/i\u003e Western Windows 98?! Things quickly take a turn once we enter the Music Room though, where we get working Japanese text next to mojibake:\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-25-SH01-Music-Room-mojibake.webp?530a7fbf\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"Screenshot of Shuusou Gyoku's Music Room in 8-bit mode on Windows 98, playing the title screen theme loaded from a BGM pack. The theme name (「秋霜玉　～ Clockworks」) and the version banner (「秋霜玉    Version 1.005     ★デモ対応版＃★」) internally use UTF-8 strings and show up correctly, but the comment uses Shift-JIS and shows up as mojibake.\"\r\n\t\u003e\u003cfigcaption\u003e\r\n\t\tIt currently also looks like this on Japanese Windows 98 due to, well, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/92\"\u003eme not having tested this case before\u003c/a\u003e.\r\n\t\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThis disparity is quickly explained: Any text that is either hardcoded or pulled from the Vorbis comment tag of a BGM pack file is in UTF-8 and can be trivially converted to UTF-16. Every piece of mojibake, on the other hand, comes from the original .DAT files, is therefore encoded in Shift-JIS, and fails the conversion to UTF-16 for the aforementioned reasons.\u003cbr\u003e\r\n\tBut seriously, how can UTF-16 text rendering suddenly just work on Windows 9x? Well, contrary to popular (or certainly my) belief, Windows 9x did have functional Unicode variants for a small group of 15 API functions, \u003ca href=\"https://www.betaarchive.com/wiki/index.php?title=Microsoft_KB_Archive/125671\"\u003ewhich just happens to include GDI's \u003ccode\u003eTextOutW()\u003c/code\u003e, \u003ccode\u003eExtTextOutW()\u003c/code\u003e, and \u003ccode\u003eGetTextExtentPoint32W()\u003c/code\u003e\u003c/a\u003e. Yup – these empty text areas we were getting for Japanese games on Windows 9x back in the day? All of them were at least partly preventable. The missing \u003ccode\u003eC_932.NLS\u003c/code\u003e on non-Japanese systems would have still meant empty text boxes if developers preferred storing text in Shift-JIS rather than UTF-8, which they might have wanted to do if their favorite editors were similarly limited. But that's about the only valid argument for using Shift-JIS on Windows 9x:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003e\u003cq\u003eCompatibility with standard \u003ccode\u003echar*\u003c/code\u003e string handling\u003c/q\u003e doesn't count (this is an argument against UTF-16, not against UTF-8)\u003c/li\u003e\r\n\t\u003cli\u003e\u003cq\u003eRendered width = (\u003ccode\u003estrlen()\u003c/code\u003e\u0026nbsp;× (full-width block size\u0026nbsp;/ 2))\u003c/q\u003e doesn't count (breaks with the proportional fonts your localizers want to use; just use \u003ccode\u003eGetTextExtentPoint32W()\u003c/code\u003e, it works on 9x too)\u003c/li\u003e\r\n\t\u003cli\u003e\u003cq\u003eB-but Han unification!1!!\u003c/q\u003e doesn't count (you control the font, and Unicode still supports lossless conversion from and to Shift-JIS)\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tSo even if devs absolutely wanted to use Shift-JIS as the on-disk format, converting to UTF-16 at runtime and calling the Unicode versions of the GDI text rendering functions would have been better than using their \u003ccode\u003e*A()\u003c/code\u003e versions. Then, Windows 9x users could have fixed empty text boxes by properly installing codepage 932, XP users would have only needed to check that one \u003ci\u003eInstall files for East Asian languages\u003c/i\u003e box, and it all would have just worked without requiring the unbearable cringe of locale emulation. The \u003ccode\u003e*A()\u003c/code\u003e versions had no reason to exist other than programmer convenience.\r\n\u003c/p\u003e\u003cp id=\"sjis-fns-2025-04-25\"\u003e\r\n\tAlright, so there's some theoretical way to get all rendered text to show up correctly on Windows 9x, regardless of locale. But what about…\r\n\u003c/p\u003e\u003ch4\u003eThe original Japanese filenames\u003c/h4\u003e\u003cp\u003e\r\n\tWithout a proper \u003ccode\u003eCreateFileW()\u003c/code\u003e, this is where we hit all the problems we were expecting. How \u003ci\u003eshould\u003c/i\u003e these behave on systems with non-Japanese codepages? Are we OK with turning old replay file names like \u003ccode lang=\"ja\"\u003e秋霜りぷEx.DAT\u003c/code\u003e into \u003ccode\u003e????Ex.DAT\u003c/code\u003e, and consequently \u003ccode\u003e____Ex.DAT\u003c/code\u003e due to question marks not being allowed in file names? This looks like the best choice we have: It's also what \u003ccode\u003eunicows.dll\u003c/code\u003e does right now, and it has the advantage of being easy to manually type.\u003cbr\u003e\r\n\tIt also is better than returning to the game's original behavior of blindly reinterpreting the bytes in the system codepage, which would turn the string into \u003ccode\u003eH‘š‚è‚ÕEx.DAT\u003c/code\u003e on codepage 1252. If you run pbg's original build on Western Windows 98, you'll see that it actually won't save \u003ci\u003eany\u003c/i\u003e file whose name starts with the \u003ccode lang=\"ja\"\u003e秋\u003c/code\u003e kanji. Apparently, 9x kernels are much stricter than NT kernels when it comes to filenames in the system codepage and will outright refuse to create a file if it contains unassigned codepoints? The Shift-JIS lead byte of \u003ccode lang=\"ja\"\u003e秋\u003c/code\u003e is \u003ccode\u003e0x8F\u003c/code\u003e, \u003ca href=\"https://en.wikipedia.org/wiki/Windows-1252\"\u003ewhich is unused in CP1252\u003c/a\u003e.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tThen again, if we had \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/17\"\u003ebetter replay-related error reporting\u003c/a\u003e, the specific file names probably wouldn't matter because we'd just display them on screen. Given that \u003ca href=\"/blog/2023-08-01#config-2023-08-01\"\u003e📝 our forward-compatible configuration format\u003c/a\u003e only uses ASCII characters on purpose and \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/36\"\u003ethe new replay format\u003c/a\u003e will do the same, this would only ever matter for the initial upgrade. There will be the possibility of converting future replays back into the original format for validation purposes, but that feature would ideally use exactly the names that the original game uses on the current system: Japanese names in Japanese locale, nothing on Western Windows 98, and mojibake everywhere else. Maybe we could even add a menu option to let players pick among all possible broken file names? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\r\n\tBut shouldn't we at least retain support for loading from original names when running the Windows 98 build on an NT kernel? Sure, the goal is a backport to Windows 98, but functional Unicode makes Windows XP a much more reasonable retro target. It sure makes a lot more sense than supporting XP with the regular, non-suffixed modern build: XP is almost identical to 98 in terms of SDL backends, being just as limited to Direct3D 9, WinMM, and DirectSound in the graphics and sound department. The only additional backend for XP would be \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/inputdev/raw-input\"\u003eRaw Input\u003c/a\u003e, and we could just enable that one conditionally.\r\n\u003c/p\u003e\u003cp id=\"sjis-unicows-2025-04-25\"\u003e\r\n\tSo how about just…\r\n\u003c/p\u003e\u003ch4\u003eLeaving it all to \u003ccode\u003eunicows.dll\u003c/code\u003e?\u003c/h4\u003e\u003cp\u003e\r\n\tYeah, why don't we just directly link both SDL and the game against the Microsoft Layer for Unicode, without relying on KernelEx injecting it for us? Then, SDL could just continue using Unicode APIs without us having to rewrite anything. And since MSLU disables itself on NT kernels, you'd still get the \u003cspan lang=\"ja\"\u003e秋霜玉\u003c/span\u003e window title and support for the original Japanese filenames regardless of the non-Unicode codepage. Heck, \u003ccode\u003eunicows.lib\u003c/code\u003e is still shipped with current Visual Studio. I'd only have to add a single linker flag and be done with it!\u003cbr\u003e\r\n\tBut when I tried this, the game broke in every environment:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eDxWnd failed to hook about half of the functions. It applied the configured window size, but failed to bypass the display mode change. There might be some permutation of tweaking options that fixes this issue, but good luck finding it. I tried all the hook-related options that sounded like they could help, but no dice.\u003c/li\u003e\r\n\t\u003cli\u003eOn Windows 98, the game crashed even when run without an external windowing tool due to an unfortunate combination of SDL and MSLU features:\u003cul\u003e\r\n\t\t\u003cli\u003eSDL's renderers can be initialized into an existing Win32 window.\u003c/li\u003e\r\n\t\t\u003cli\u003eThis means that SDL will handle all events that the system sends to this window.\u003c/li\u003e\r\n\t\t\u003cli\u003eHowever, the window might have set up a custom \u003ca href=\"https://en.wikipedia.org/wiki/WindowProc\"\u003ewindow proc\u003c/a\u003e to handle certain events outside of SDL. It might be a good idea to still run this code and not have SDL take exclusive control.\u003c/li\u003e\r\n\t\t\u003cli\u003eDue to the whole ANSI-vs.-Unicode mess, doing this requires a dedicated \u003ci\u003esubclassing\u003c/i\u003e mechanism. By using \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-callwindowprocw\"\u003e\u003ccode\u003eCallWindowProc()\u003c/code\u003e\u003c/a\u003e on \u003ca href=\"https://devblogs.microsoft.com/oldnewthing/20031201-00/?p=41673\"\u003emagic cookie values\u003c/a\u003e, Windows allows ANSI window procs to subclass Unicode window procs and vice versa.\u003c/li\u003e\r\n\t\t\u003cli\u003eHowever, MSLU uses the same subclassing mechanism to provide Unicode↔codepage wrappers for all events that carry string data. Since it hooks window creation, it goes first, subclassing the window proc that SDL specified in the window class structure.\u003c/li\u003e\r\n\t\t\u003cli\u003eThen, it's SDL's turn to check whether it needs to subclass the window. It would only need to do so if the window proc differs from its own, which should only ever be the case if the window wasn't created through SDL, but, um…\u003cfigure\u003e\r\n\t\t\t\u003cpre\u003e// Remember the previous window proc in case we have to subclass it\r\nWNDPROC superclass_wndproc = GetWindowLong(hwnd, GWL_WNDPROC);\r\nif (superclass_wndproc == SDL_WIN_WindowProc) {\r\n    // Window uses our window class and wasn't subclassed. Nothing to do.\r\n    superclass_wndproc = NULL;\r\n} else {\r\n    // Window already has a foreign window proc. Move us back to the top\r\n    // of the hierarchy to ensure that we handle the messages we care\r\n    // about. Surely no one subclassed us between CreateWindow() and now?\r\n    SetWindowLong(hwnd, GWL_WNDPROC, SDL_WIN_WindowProc);\r\n}\u003c/pre\u003e\u003cfigcaption\u003e\r\n\t\t\tAdapted from \u003ccode\u003eSDL_windowswindow.c\u003c/code\u003e.\r\n\t\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\r\n\t\t\u003cli\u003eNow, both MSLU and SDL think that their own window proc is a subclass of the other one. Thus, both of them call the other one using \u003ccode\u003eCallWindowProc()\u003c/code\u003e, expecting it to terminate the chain…\u003c/li\u003e\r\n\t\t\u003cli\u003e…but since neither of them does, the resulting infinite recursion ends up crashing the game with a stack overflow. 💥\u003c/li\u003e\r\n\t\u003c/ul\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tMaybe this could be considered a fixable bug that traces back to SDL 1, but the whole situation is just very silly. If this had worked, we would be running the Windows 9x build through up to three separate layers of dynamically patched code – KernelEx, MSLU, and D3DWindower – when we'd like to have at most one and ideally zero. Besides, we \u003ci\u003eknow\u003c/i\u003e which code we want to run, we \u003ci\u003eknow\u003c/i\u003e that we don't need to reach for subclassing to make it all work, and we \u003ci\u003eknow\u003c/i\u003e that SDL is the ideal place for it. Now we just have to write it all.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tUntil then, this is where we are right now:\r\n\u003c/p\u003e\u003cp\u003e\r\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0310\"\u003e\r\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0310 Windows build\u003c/a\u003e\r\n\u003c/p\u003e\u003cp\u003e\r\n\tNext up: The long-awaited return to TH03! With \u003cscript\u003eformatCurrency(34500)\u003c/script\u003e\u003cnoscript\u003e345.00\u0026nbsp;€\u003c/noscript\u003e per month going explicitly toward that game now, we'll definitely stay there for a while. Ember2528 is generously funding short-term and long-term netplay options, so let's finish \u003ccode\u003eOP.EXE\u003c/code\u003e in preparation for nice and user-friendly menus. This is the last main menu to be decompiled across all of PC-98 Touhou and it's mostly text-based, so how hard can it be?\r\n\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-05-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-04-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-04-25T23:37:38Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-04-09",
      "url": "https://rec98.nmlgc.net/blog/2025-04-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-04-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-02-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-04-09\"\u003e\u003ctime datetime=\"2025-04-09T03:03:14Z\"\u003e2025-04-09 03:03\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0307\"\u003eP0307\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (SDL 3 platform layer)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0303-2...a4adcbb\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0308\"\u003eP0308\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Render API unbricking / ReC98 build label on the title screen / Revamped pixel format handling)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/a4adcbb...c62f7ad\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0309\"\u003eP0309\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (WebP screenshot compression / Compression benchmark in the main menu)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/c62f7ad...P0309\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/policy-bugfix\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\r\n\tfigure.main-menu-2025-04-09 img {\r\n\t\tbackground-image: url('/static/sh01-main-menu-bg.png?f38e9fd7');\r\n\t}\r\n\r\n\t.benchmark-2025-04-09 table td:not(:first-child) {\r\n\t\twidth: 7ch;\r\n\t}\r\n\r\n\t.benchmark-2025-04-09 table td[colspan=\"10\"] {\r\n\t\ttext-align: center;\r\n\t}\r\n\u003c/style\u003e\r\n\r\n\u003cp\u003e\r\n\tWell, that fell apart surprisingly quickly. The release of Shuusou Gyoku's Linux port just happened to be surrounded by the unluckiest sequence of events in Arch Linux land:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eJan. 21: The SDL team releases \u003ca href=\"https://github.com/libsdl-org/SDL/releases/tag/release-3.2.0\"\u003ethe first stable version of SDL 3\u003c/a\u003e\u003c/li\u003e\r\n\t\u003cli\u003eJan. 24: \u003ca href=\"https://gitlab.archlinux.org/archlinux/packaging/packages/sdl3/-/commit/588f458e053d1bbc056645f660ba68551d39eec3\"\u003eArch Linux packages SDL 3\u003c/a\u003e\u003c/li\u003e\r\n\t\u003cli\u003eJan. 25: I release \u003ca href=\"/blog/2025-01-25\"\u003e📝 the first version of Shuusou Gyoku's Linux port\u003c/a\u003e, completing the SDL 2 porting work I started in 2023\u003c/li\u003e\r\n\t\u003cli\u003eJan. 28: Arch Linux \u003ca href=\"https://gitlab.archlinux.org/archlinux/packaging/state/-/commit/1b9097aa4fa40c9946d7555985bef140da82a61f\"\u003eremoves SDL 2\u003c/a\u003e and replaces it with \u003ca href=\"https://gitlab.archlinux.org/archlinux/packaging/packages/sdl2-compat/-/commit/bf936adbb3e594f879bb300800bd6ef12fe1db30\"\u003ethe previously packaged sdl2-compat\u003c/a\u003e, a compatibility layer that is meant to perfectly implement the SDL 2 API on top of SDL 3. In reality, though, it \u003ca href=\"https://bbs.archlinux.org/viewtopic.php?id=302982\"\u003ebroke\u003c/a\u003e \u003ca href=\"https://bbs.archlinux.org/viewtopic.php?id=303005\"\u003elots\u003c/a\u003e \u003ca href=\"https://github.com/libsdl-org/sdl2-compat/issues/255\"\u003eof\u003c/a\u003e \u003ca href=\"https://github.com/libsdl-org/sdl2-compat/issues/253\"\u003eapplications\u003c/a\u003e including my Shuusou Gyoku port, and turned their users into \u003ca href=\"https://gitlab.archlinux.org/archlinux/packaging/packages/sdl2-compat/-/issues/1\"\u003equite disgruntled beta testers\u003c/a\u003e.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tAfter a \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/policy-bugfix\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/a\u003e\u003c/span\u003e for \u003ca href=\"https://github.com/nmlgc/ssg/commit/c11a93c6c7600d2a158a4739397ca634a8f7862a\"\u003ea silly mistake on my part\u003c/a\u003e, Shuusou Gyoku was still \u003ci\u003eplayable\u003c/i\u003e on sdl2-compat as it was only affected by rather minor bugs, but these bugs still undermined the effort I put into the port. That left us with three options:\r\n\u003c/p\u003e\u003col\u003e\r\n\t\u003cli\u003eLet the more involved SDL community fix sdl2-compat out on their own. After all, why should \u003ci\u003ewe\u003c/i\u003e bother if rogue distros randomly mess with our dependencies?\u003c/li\u003e\r\n\t\u003cli\u003eBecome part of that community and help fix the issues in either sdl2-compat or SDL 3.\u003c/li\u003e\r\n\t\u003cli\u003eProperly update Shuusou Gyoku to SDL 3 right now, while keeping SDL 2 support for the Flatpak, more conservative Linux distributions, and the upcoming Windows 98 backport.\u003c/li\u003e\r\n\u003c/ol\u003e\u003cp\u003e\r\n\tI really would have preferred to delay this migration for a few years until the dust has settled. For this project, I already picked C++ as the dependency I want to be on the bleeding edge of, and SDL 2 was supposed to balance this out by being the conservative and stable choice. Oh well, if we've got to update at \u003ci\u003esome\u003c/i\u003e point, we might as well do it now. The ReC98 development schedule at least gave me another month of waiting for the community to sort out SDL 3's growing pains…\r\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#sdl2-compat-2025-04-09\"\u003eForced onto an unstable SDL 2 compatibility layer\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl3-2025-04-09\"\u003eUpdating to SDL 3\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#format-2025-04-09\"\u003ePicking a screenshot format\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#qoi-2025-04-09\"\u003eQOI, the expected disappointment\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#jxl-2025-04-09\"\u003eJPEG XL, the unexpected disappointment\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#bench-2025-04-09\"\u003eBenchmark results\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#webp-2025-04-09\"\u003eLossless WebP\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#effort-2025-04-09\"\u003eLetting players pick an effort level\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#perf-2025-04-09\"\u003eFuture performance improvements\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tag-2025-04-09\"\u003eRendering the build ID with some unused glyphs\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"sdl2-compat-2025-04-09\"\u003e\u003cp\u003e\r\n\tSo, why does something like sdl2-compat even exist if it only causes problems? And why are distros rolling it out so soon after SDL 3 if SDL 2 has been working fine all the time? In a nutshell, sdl2-compat is the second pillar in SDL's forward compatibility strategy. While the \u003ca href=\"/blog/2023-09-30#sdl-2023-09-30\"\u003e📝 \u003ci\u003edynamic API\u003c/i\u003e mechanism\u003c/a\u003e ensures compatibility with future \u003ci\u003eminor\u003c/i\u003e versions by integrating dynamic linking so deeply that static linking is made entirely useless, sdl\u003cvar\u003eN\u003c/var\u003e-compat ensures compatibility with one future \u003ci\u003emajor\u003c/i\u003e version by implementing version \u003cvar\u003eN\u003c/var\u003e's API in terms of SDL version \u003cvar\u003eN+1\u003c/var\u003e. This allows the SDL team to very quickly stop updating version \u003cvar\u003eN\u003c/var\u003e while still allowing programs linked against that version to run well on modern systems by using all the actively maintained backends of version \u003cvar\u003eN+1\u003c/var\u003e. This worked out well with \u003ca href=\"https://github.com/libsdl-org/sdl12-compat\"\u003esdl12-compat\u003c/a\u003e, which nowadays seems to do a great job at preserving abandoned SDL 1 games – especially if we consider that you'd be running sdl12-compat on top of sdl2-compat on top of SDL 3 from now on. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\r\n\u003c/p\u003e\u003cp\u003e\r\n\tSo it only makes sense why the SDL developers would want to repeat this success story with the transition from SDL 2 to 3. The problem is that \u003ca href=\"https://github.com/libsdl-org/sdl2-compat/blob/fe43e143dd9c143bbbbc24a6daad6713f11af184/README.md\"\u003ethey're already selling sdl2-compat as a perfect drop-in replacement for proper SDL 2\u003c/a\u003e, and wanted to push it onto people \u003ca href=\"https://github.com/libsdl-org/SDL/issues/11047#issuecomment-2395565043\"\u003eeven before SDL 3 was officially released\u003c/a\u003e. The sales pitch follows their usual \"trust me bro\" rhetoric:\r\n\u003c/p\u003e\u003cblockquote\u003eIf you absolutely must have the real SDL2 (\"SDL 2 Classic\"), please use the SDL2 branch at \u003ca href=\"https://github.com/libsdl-org/SDL\"\u003ehttps://github.com/libsdl-org/SDL\u003c/a\u003e, which occasionally gets bug fixes (and eventually, no new formal releases). But we strongly encourage you not to do that.\u003c/blockquote\u003e\u003cp\u003e\r\n\tFollowed by zero arguments to back up this audacious suggestion. So they not only imply that sdl2-compat is already perfectly compatible and works without bugs for every SDL 2 program ever, but also that the underlying SDL 3 implementation doesn't introduce any bugs on top – and it only takes a single look into either project's issue tracker to disprove that notion. There is no technical reason why a distro couldn't ship SDL 3 and 2 in parallel. The continued existence of the \u003ca href=\"https://aur.archlinux.org/packages/sdl2\"\u003eSDL 2 AUR package\u003c/a\u003e is proof of that, and still received upset comments as of mid-\u003ci\u003eMarch\u003c/i\u003e that justified its existence.\u003cbr\u003e\r\n\tThere was absolutely no reason to push sdl2-compat on everyone by default other than forcefully turning users into beta testers. SDL 2 was still stable, maintained, and working well. People who needed SDL 3 before its release for whatever feature already used SDL 3. People who want to use the SDL 3 backends to solve some obscure backend-related issue in an SDL 2 program can use sdl2-compat without needing it to be the only option available. And with a package size of 1.2\u0026nbsp;MiB, you can't convince me that SDL 2 is somehow a burden on the packaging front either – especially if your distro has separate packages for every commonly used fiddly \u003ca href=\"https://archlinux.org/packages/?sort=\u0026q=python-\"\u003ePython\u003c/a\u003e and \u003ca href=\"https://archlinux.org/packages/?sort=\u0026q=haskell-\"\u003eHaskell\u003c/a\u003e library.\u003cbr\u003e\r\n\tI can't help but imagine the reaction if Microsoft pushed an enforced update of this magnitude. They're already getting regularly lambasted by the press for much smaller and ultimately inconsequential offenses…\r\n\u003c/p\u003e\u003cp\u003e\r\n\tFor all the \u003ca href=\"/blog/2025-01-25#flatpak-2025-01-25\"\u003e📝 criticism I had about Flatpak and Flathub last time\u003c/a\u003e, they made the right choice of not treating their base package as a rolling and bleeding-edge distribution. The Freedesktop platform will only ship SDL 3 in its next version releasing in August, which will probably leave enough time for the SDL developers to address all but the rarest remaining issues in sdl2-compat. Although I'm not sure how I should interpret \u003ca href=\"https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/commit/50d191db666248abe6883bfe07927e12dfa3889f\"\u003ethis commit being made at that specific time\u003c/a\u003e: This is either very considerate (because they've chosen to take up the job of early-adopting SDL 3 as part of developing the new SDK version, and thus will be helping out with reporting bugs), or very inconsiderate because they bought the whole sdl2-compat story just like Arch did. If Freedesktop SDK updates shipped in February rather than August and the release tag was on this branch, they would have screwed over their users just as much. Also, there's \u003ci\u003estill\u003c/i\u003e not much point in force-updating everyone onto a compatibility layer \u003ci\u003ein freaking 2025\u003c/i\u003e…\r\n\u003c/p\u003e\u003cp\u003e\r\n\tThen again, I can empathize with the SDL developers to a degree. Lots of developers have been asking the \u003ci\u003e\"when is SDL 3 ready and stable enough for regular use?\"\u003c/i\u003e question while picturing SDL as this highly important and central library that surely has a big team of testers who could ensure its stability at one point. But if there just isn't enough Valve money to form such a team, what else should you do as a developer other than turn your personal hype into a \u003ci\u003e\"it's ready now, go use it and please leave feedback\"\u003c/i\u003e reply? Maybe, turning your users into beta testers is the only realistic way to ever approach stability in this economy. And sure, they \u003ci\u003ecall\u003c/i\u003e it \u003ci\u003e\u003cq\u003e3.2.0\u003c/q\u003e\u003c/i\u003e for… \u003ca href=\"https://discourse.libsdl.org/t/how-soon-will-the-first-stable-version-of-sdl3-be-released/46024/3\"\u003ereasons\u003c/a\u003e, but \u003ca href=\"https://www.phoronix.com/news/SDL3-Official-Release\"\u003ethey're not fooling anyone\u003c/a\u003e.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tThe big irony, however, is this: At one point in the future, sdl2-compat \u003ci\u003ewill\u003c/i\u003e be that perfect solution for running abandoned SDL 2 (and SDL 1) programs on top of SDL 3. But it's the exact opposite of what you'd want during active development: You \u003ci\u003ewant\u003c/i\u003e to update to SDL 3 and use the new APIs and function names to be ready for the future, but also retain the option to run on the stable SDL 2 foundation for at least a little longer until every distribution has caught up. Or, in other words, you want to run SDL 3 on top of SDL 2.\u003cbr\u003e\r\n\tYou could totally have a library that implements this alternate kind of compatibility layer. It would still be prone to bugs just like sdl2-compat, but unlike that one, the \u003ci\u003echance\u003c/i\u003e for new bugs is halved since you'd be running on top of the proven and stable SDL 2. But of course, such a library would restrict your codebase to SDL 2's feature set, which is probably why something like this doesn't exist. So instead, our SDL platform layer now contains 64 conditional branches and \u003ca href=\"https://github.com/nmlgc/ssg/blob/b159c604388a89d0d64efa2d49cbe0f34b0c09d4/platform/sdl/sdl2_wrap.h\"\u003ea bunch of function renaming macros and generic helper code\u003c/a\u003e to support compiling against both SDL 3 and SDL 2. At least I wrote it all in a way that allows us to quickly rip out SDL 2 support once we no longer need it…\r\n\u003c/p\u003e\u003chr id=\"sdl3-2025-04-09\"\u003e\u003cp\u003e\r\n\tOh well, enough ranting. Because once it works, there are plenty of things to like about SDL 3. Limited to, of course, everything notable that applies to Shuusou Gyoku:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eRequesting fullscreen from SDL 3's basic window creation API will now \u003ca href=\"https://github.com/libsdl-org/SDL/blob/33f90f2e41a1567b2d40c00e8e8869e9573ca4e5/src/video/SDL_video.c#L2475-L2485\"\u003ealways give you a borderless window\u003c/a\u003e as they went with the times and removed the option to directly create a window in exclusive fullscreen mode. In isolation, this might look bad enough to not even consider updating to SDL 3. However, this doesn't mean that boomer fullscreen is \u003ci\u003egone\u003c/i\u003e – it only has been relegated to \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_SetWindowFullscreenMode\"\u003ea separate and, in fact, much more comprehensive mode-changing API\u003c/a\u003e that \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_GetClosestFullscreenDisplayMode\"\u003ealso covers refresh rates\u003c/a\u003e. Using it does require significantly more and different code compared to SDL 2, but being explicit about the refresh rate is crucial for games whose speed depends on the frame rate, like this one. If your display supports a 62.5\u0026nbsp;Hz mode by any chance, we select it now.\u003c/li\u003e\r\n\t\u003cli\u003eSDL 3's software blitters come with optimized SSE2, SSE4.1, and AVX implementations, replacing SDL 2's aging \u003ca href=\"https://github.com/libsdl-org/SDL/issues/5918\"\u003eand nowadays actually suboptimal\u003c/a\u003e MMX code paths. On the surface, this only seems to speed up the software renderer as far as we're concerned, but it will also be very welcome once we have to do pixel format conversions. (Which, spoiler, I managed to just barely avoid on the SDL level for this new code.)\u003c/li\u003e\r\n\t\u003cli\u003eThe new \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_SetRenderLogicalPresentation\"\u003e\u003ccode\u003eSDL_SetRenderLogicalPresentation()\u003c/code\u003e\u003c/a\u003e function now implements all of the three borderless fullscreen layouts as part of SDL. Together with the now \u003ca href=\"https://github.com/libsdl-org/sdlwiki/pull/592\"\u003ecleaned-up handling of render target state\u003c/a\u003e, this removes almost all of the complexity and state juggling that SDL 2 previously required for the combination of fullscreen and clipping. Too bad that I still have to retain all of that SDL 2 code for the time being…\u003c/li\u003e\r\n\t\u003cli\u003eThe \u003ca href=\"https://wiki.libsdl.org/SDL3/CategoryFilesystem\"\u003efilesystem API\u003c/a\u003e that originated in SDL 2 is finally joined by \u003ca href=\"https://wiki.libsdl.org/SDL3/CategoryIOStream\"\u003ea matching set of file access functions\u003c/a\u003e that Do The Right Thing, explicitly take UTF-8 filenames, and use the Unicode APIs on Windows. If this had existed \u003ca href=\"/blog/2022-12-31\"\u003e📝 at the end of 2022\u003c/a\u003e, I wouldn't have felt the need to write my own abstractions. Sure, the lack of UTF-16 overloads means that this API is not \u003ci\u003estrictly\u003c/i\u003e, \u003ci\u003eperfectly\u003c/i\u003e optimal on Windows, but in turn, we get this API for free with the rest of SDL. It'll even be very welcome for the Windows 9x port, which could simply translate UTF-8 to the system codepage without requiring \u003ca href=\"https://learn.microsoft.com/en-us/archive/msdn-magazine/2001/october/mslu-develop-unicode-applications-for-windows-9x-platforms-with-the-microsoft-layer-for-unicode\"\u003eany other kind of Unicode layer\u003c/a\u003e. Besides, I've found myself using these \u003cq\u003estrictly optimal\u003c/q\u003e UTF-16 strings less and less: These have always been an implementation detail of the Windows version, and any path we save in a .CFG file should better be in UTF-8 to allow configuration sharing between Linux and Windows.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e, the \"screenshot\" function that transfers pixel data from the GPU to system memory, now allocates a new pixel surface instead of writing pixel data in a specific format to pre-allocated memory. This is another change that looks bad on the surface because we sure love them freedoms to self-allocate our memory in C/C++ land. However:\u003col\u003e\r\n\t\t\u003cli\u003eThis single allocation is far from being the bottleneck in the screenshotting process. It doesn't even clearly stick out in execution timings because it gets completely masked by the variance of the actual GPU→CPU pixel transfer.\u003c/li\u003e\r\n\t\t\u003cli\u003eIn SDL 2's version of the function, you decided the pixel format that SDL would write into your buffer, which might have incurred a conversion if your chosen format didn't match the pixels returned by the GPU. In Shuusou Gyoku, this could have easily happened with geometry scaling. By newly allocating the returned surface, SDL 3 can keep the original pixel format and thus needs to involve at most a single \u003ccode\u003ememcpy()\u003c/code\u003e – which is \u003ci\u003ealways\u003c/i\u003e measurably faster than converting pixels, even if that conversion is SIMD-optimized.\u003c/li\u003e\r\n\t\t\u003cli\u003eNot even having the option to overthink memory pre-allocation sure simplifies your code a lot.\u003c/li\u003e\r\n\t\u003c/ol\u003e\u003c/li\u003e\r\n\t\u003cli\u003eGraphics APIs are now addressed by their identifier string rather than their index within the platform-specific list of APIs. SDL 2 has always provided ways to map between both indices and strings, but the fact that every function now takes a string is a nice way of nudging developers to use strings in their configuration as well. They would allow a user's API selection to be retained independently of the SDL developers later changing the order of that list – once I adapt our config format from numbers to strings in a future release, that is. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca href=\"https://github.com/libsdl-org/SDL/issues/7462\"\u003eThis unassuming change to the OpenGL defaults on Windows\u003c/a\u003e removed \u003ca href=\"https://github.com/libsdl-org/SDL/issues/11041\"\u003ethe seemingly unfixable mode change for borderless fullscreen\u003c/a\u003e on some displays! 🙌\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tA few changes have good and bad elements:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eSDL apps can now define \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_SetAppMetadataProperty\"\u003emetadata strings\u003c/a\u003e. Most of these currently don't do anything, but the \u003ci\u003eidentifier\u003c/i\u003e now gets used as the Wayland and X11 window class name and thus represents a much cleaner way of having class-derived icons than \u003ca href=\"/blog/2025-01-25#icon-linux-2025-01-25\"\u003e📝 the previous undocumented \u003ccode\u003eSDL_VIDEO_X11_WMCLASS\u003c/code\u003e environment variable\u003c/a\u003e. But if you read that post again, my main issue wasn't SDL's implementation, but the fact that support for class-derived icons is so rare among window managers to begin with. Not only does this change not help the situation, but it arguably makes it even worse due to a slightly different mapping decision: The app identifier is assigned to the \u003ccode\u003eWM_CLASS\u003c/code\u003e \u003ci\u003eclass\u003c/i\u003e name, but the additional \u003ci\u003einstance\u003c/i\u003e name receives the binary's file name, which unfortunately breaks class-derived icons in IceWM where the instance name takes precedence.\u003c/li\u003e\r\n\t\u003cli\u003eDraw calls are now batched on all renderers, and batching can no longer be deactivated. \u003ca href=\"/blog/2024-10-22#benchmark-2024-10-22\"\u003e📝 During my previous experiments\u003c/a\u003e, SDL's Direct3D 11 backend turned out to be by far the fastest batching renderer on Windows, and SDL 3 coincidentally also made it the new default. So it makes sense to follow suit and remove our previous OpenGL override, restoring \u003ca href=\"/blog/2024-10-22#lines-2024-10-22\"\u003e📝 pixel-perfect line rendering in framebuffer-scaled mode\u003c/a\u003e by default.\u003cbr\u003e\r\n\tThe massive downside, however, is that the combination of framebuffer rendering and OpenGL ES 2 is now completely broken on integrated Intel graphics, in the worst way: The game initializes fine and responds to input, but only shows a black screen. If we offer such a menu, we'd better also have a feature to unbrick your game in a non-graphical way if it only renders a black screen. That's why you now can\u003cul\u003e\r\n\t\t\u003cli\u003epress F7 to cycle through the list of APIs at any point, or\u003c/li\u003e\r\n\t\t\u003cli\u003euse the environment variable \u003ccode\u003eSDL_RENDER_DRIVER\u003c/code\u003e to override any previous manual API selection, which didn't work before.\u003c/li\u003e\r\n\t\u003c/ul\u003e\u003c/li\u003e\r\n\t\u003cli\u003eDraw call batching even extends to the software renderer now, for some reason. Doesn't software rendering boil down to nothing more than writing pixels into a system-memory buffer on a single thread? There's no penalty for just \u003ci\u003edoing\u003c/i\u003e the thing, but there certainly is a small penalty for gathering all the things into a queue. I'd rather not pepper that procedural mess of a graphics backend with even more imperative function calls, but you can make just as much of an argument for the consistency of requiring a flush regardless of whether a renderer represents software or hardware.\u003c/li\u003e\r\n\t\u003cli\u003eThe new Vulkan and GPU render backends are perhaps the most exciting change for a certain group of people. The GPU API in particular provides an abstraction for the common modern paradigm of command buffers and shaders, which is shared among Vulkan, Direct3D 12, and Metal. \u003ca href=\"https://github.com/libsdl-org/SDL/issues?q=GPU\"\u003eGiven the amount of attention it received\u003c/a\u003e, this feature is undoubtedly great for everyone developing modern games. However, not only couldn't we care less for a game of this vintage, but it's also just more of the same dilemma: While more backends \u003ci\u003ecan\u003c/i\u003e offer a higher chance of the game working well on some potato out there, they primarily mean more code surface, which means \u003ca href=\"https://github.com/libsdl-org/SDL/issues/12646\"\u003emore bugs\u003c/a\u003e.\u003cul\u003e\r\n\t\t\u003cli\u003eLuckily, the GPU API was so much of a success that \u003ca href=\"https://github.com/libsdl-org/SDL/issues/12554\"\u003ethe SDL team is thinking about removing SDL_Renderer's non-GPU Direct3D 12, Vulkan, and Metal backends\u003c/a\u003e. All of these implement the immediate SDL_Renderer API in terms of command buffers and shaders, so it makes perfect sense to just replace these specific implementations with the single GPU abstraction that in turn uses any one of the three APIs under the hood. Ideally, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/83\"\u003ethe API menu should then also offer players to choose this second layer of backends\u003c/a\u003e once SDL has taken that step.\u003c/li\u003e\r\n\t\u003c/ul\u003e\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tThankfully, the list of entirely bad changes is quite short:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eAll API functions now return \u003ccode\u003etrue\u003c/code\u003e/nonzero on success and \u003ccode\u003efalse\u003c/code\u003e/zero on failure, rather than 0 on success and \u0026lt;0 on failure as in SDL 2. Sure, \u003ccode\u003etrue\u003c/code\u003e\u0026nbsp;= success makes intuitive sense when you just start out programming, but then you realize that the overwhelming majority of functions can fail in multiple ways and success is just the absence of failure. SDL 2 got the right idea about this, but SDL 3 chose to regress to said beginner levels because \u003ca href=\"https://github.com/libsdl-org/SDL/issues/10575\"\u003eSam Lantinga got increasingly convinced of this idea that he, and everyone else, initially considered horrible\u003c/a\u003e.\u003c/li\u003e\r\n\t\u003cli\u003e\u003ccode\u003e#include\u003c/code\u003e directives must now be prefixed with an explicit \u003ccode\u003eSDL3/\u003c/code\u003e path, unlike SDL 2 which didn't use a prefix. This was \u003ca href=\"https://github.com/libsdl-org/SDL/issues/6575\"\u003eapparently necessary to fulfill some macOS requirement\u003c/a\u003e, but they've also removed the path from their \u003ccode\u003epkg-config --cflags\u003c/code\u003e, turning the prefixed syntax into the only sanctioned cross-platform way of including SDL 3's headers. Being able to compile SDL3-using code without any additional \u003ccode\u003eCFLAGS\u003c/code\u003e might look pretty, but no sane build system is going to make an exception and \u003ci\u003enot\u003c/i\u003e call \u003ccode\u003epkg-config --cflags\u003c/code\u003e as it does for any other external library. And now I have to duplicate the \u003ccode\u003e#include\u003c/code\u003e section in every translation unit for the SDL 2 code path…\u003c/li\u003e\r\n\t\u003cli\u003eAll SDL threads must now be \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_WaitThread\"\u003emanually awaited\u003c/a\u003e before calling \u003ccode\u003eSDL_Quit()\u003c/code\u003e. If they aren't, SDL reports a \"leaked thread\" even if the underlying OS thread might have cleanly finished. I get it, \u003ca href=\"https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/\"\u003estructured concurrency\u003c/a\u003e is probably a good idea, but it only works naturally if the rest of your program is structured accordingly, which doesn't apply to this 25-year-old codebase. Enforcing this leak check just forces me to write cleanup code for the sole purpose of satisfying SDL's bookkeeping to avoid that error.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tStill, the constant stumbling over bugs and \u003ca href=\"https://github.com/libsdl-org/SDL/issues/12432#issuecomment-2698313170\"\u003edeliberate instabilities\u003c/a\u003e made this take way longer than it had any right to. For three of these bugs, \u003ca href=\"https://github.com/libsdl-org/SDL/issues?q=author%3Anmlgc\"\u003eI was the first one to report them\u003c/a\u003e, and I could have even reported a fourth one if I actually cared about Vulkan and didn't happen to find a workaround right before I pushed out the release.\u003cbr\u003e\r\n\tWith the additional API unbricking feature, we've ended up well into a second push. Replays were too big of a feature for now, but screenshot compression sounded like a nice task for the rest of that push. Really, how hard can it be? Add reference C library of our encoder of choice, call API with pixel buffer we get from SDL, write compressed pixel buffer to file. Easy, right? Well…\r\n\u003c/p\u003e\u003chr id=\"format-2025-04-09\"\u003e\u003cp\u003e\r\n\tFor starters, which format do we choose? Ember2528 had a clear preference, but it makes sense to compare it against other contenders first. There will be a complete benchmark further below, but let's get the seemingly most obvious candidate out of the way first:\r\n\u003c/p\u003e\u003ch3 id=\"qoi-2025-04-09\"\u003eQOI\u003c/h3\u003e\u003cp\u003e\r\n\tBecause who doesn't want a fast encoder for a simple format with \u003ca href=\"https://github.com/phoboslab/qoi/?tab=readme-ov-file#qoi-support-in-other-software\"\u003esteadily growing adoption\u003c/a\u003e? Sure, \u003ca href=\"https://www.youtube.com/watch?v=EFUYNoFRHQI\u0026t=1383s\"\u003epart of the adoption might be hype-driven\u003c/a\u003e, but as far as hype goes, there are definitely worse targets than a codec that fits in less than 300 lines of C. The low-color images we want to compress are rather simple from a modern point of view as well, so you'd expect QOI to be a perfect match…\u003cbr\u003e\r\n\t…until you actually try encoding a few representative images and are greeted with file sizes that are \u003ci\u003eway\u003c/i\u003e further removed from PNG than you'd expect after seeing the \u003ca href=\"https://qoiformat.org/benchmark/\"\u003eofficial benchmarks\u003c/a\u003e. Since the \u003ca href=\"https://qoiformat.org/qoi-specification.pdf\"\u003especification\u003c/a\u003e is short enough, we can easily explain these results:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eAll of Shuusou Gyoku's sprites are intended to be rendered within a palettized 256-color framebuffer. 3D-rendered gradients and transparency will drive up the number of unique colors in screenshots into the low 4-digit range at times, but it still makes sense to assume uncompressed 8-bit BMPs as the baseline. At our native resolution of 640×480, these are 308,278 bytes large. This is what we expect our chosen codec to beat, by hopefully a quite significant margin.\u003c/li\u003e\r\n\t\u003cli\u003eThe 32-bit \u003ccode\u003eQOI_OP_RGB\u003c/code\u003e chunk would already blow up each affected pixel to 4× the size it would have had in a palettized image. Let's hope that the QOI encoder largely uses this chunk to define palette colors, and that we don't get to see it that often otherwise.\u003c/li\u003e\r\n\t\u003cli\u003eThe 16-bit \u003ccode\u003eQOI_OP_LUMA\u003c/code\u003e chunk can \u003ci\u003emaybe\u003c/i\u003e help compress unknown pixels that haven't yet been put into the running palette, but would still not contribute any compression compared to our baseline size. Fortunately, we shouldn't see too many of those as the encoder is specified to prefer 8-bit chunks where possible…\u003c/li\u003e\r\n\t\u003cli\u003e…except that \u003ccode\u003eQOI_OP_INDEX\u003c/code\u003e spends 8 bits on encoding a 6-bit palette index. With only 64 colors in the palette rather than the 256 we want, we're bound to see a lot more of those bulky 32-bit \u003ccode\u003eQOI_OP_RGB\u003c/code\u003e chunks after all. Not to mention the fact that colors are mapped onto these 64 palette slots using a simple multiplicative hash that will cause collisions at regular color intervals.\u003c/li\u003e\r\n\t\u003cli\u003eAny compression gains over uncompressed 8-bit BMP would therefore come from \u003ccode\u003eQOI_OP_RUN\u003c/code\u003e. If run-length encoding is the best an image codec can do, that's rather basic instead of OK, I'd say.\u003cul\u003e\u003cli\u003e\r\n\t\tActually… wait a moment, \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/gdi/bitmap-compression\"\u003edoesn't BMP also have a run-length-encoded mode that was mostly forgotten after the 90s\u003c/a\u003e? And indeed, the compression rates between vintage BMP/RLE and QOI are very similar, with any differences stemming from the way these two formats encode their run lengths. QOI typically does slightly better, but BMP/RLE still beats it in the \u003cspan lang=\"ja\" style=\"word-break: keep-all;\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e logo and the main menu.\r\n\t\u003c/li\u003e\u003c/ul\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tSo while reduced complexity and blazingly fast encoding speed are good arguments, they don't cut it if decent compression of our source images relies on all the complexity found in PNG. But shouldn't this deficiency have stuck out in the \u003ca href=\"https://qoiformat.org/benchmark/\"\u003eofficial benchmark\u003c/a\u003e in some way? After all, 43% of the images in QOI's test suite have ≤256 colors, with most of them coming from \u003ca href=\"https://www.philipk.net/index.html\"\u003ePhilip K's Ancient Collection\u003c/a\u003e in the \u003ccode\u003etextures_pk\u003c/code\u003e directory, where they make up 80%. For this directory, the official numbers claim average compressed sizes of 80\u0026nbsp;KiB for PNG and 75\u0026nbsp;KiB for QOI, and running the benchmark myself confirms these numbers…\u003cbr\u003e\r\n\t…but wait, the input PNG files in the test suite package are actually half that size?! Yup – this benchmark merely tests the fixed, untunable QOI \u003ci\u003eformat\u003c/i\u003e against two specific PNG \u003ci\u003eencoders\u003c/i\u003e, libpng and stb_image, at their default compression level and filter settings. It does \u003ci\u003enot\u003c/i\u003e claim anything about QOI's relation to the known limits of PNG as a format, \u003ca href=\"https://youtu.be/EFUYNoFRHQI?t=1702\"\u003edespite what the hype drivers would lead you to conclude all too easily\u003c/a\u003e. In any case, it paints a much different picture of QOI's 256-color capabilities:\r\n\u003c/p\u003e\u003cfigure class=\"benchmark-2025-04-09\"\u003e\r\n\t\u003ctable class=\"numbers\"\u003e\u003cthead\u003e\r\n\t\t\u003ctr\u003e\u003cth\u003e\u003c/th\u003e\u003cth\u003eAverage file size\u003c/th\u003e\u003c/tr\u003e\r\n\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\u003ctr\u003e\u003ctd\u003estb_image\u003c/td\u003e\u003ctd\u003e110,337\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003ctr\u003e\u003ctd\u003elibpng\u003c/td\u003e\u003ctd\u003e82,136\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd\u003e77,404\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003ctr\u003e\u003ctd\u003ePNG source files\u003c/td\u003e\u003ctd\u003e43,437\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e41,032\u003c/td\u003e\u003c/tr\u003e\r\n\t\u003c/tbody\u003e\u003c/table\u003e\r\n\t\u003cfigcaption\u003eWe will later see why comparing the slowest PNG encoders against the constantly fast QOI is, in fact, not unfair.\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThe final nail in QOI's coffin is this concession at the end of its release announcement:\r\n\u003c/p\u003e\u003cblockquote\u003eSIMD acceleration for QOI would also be cool but (from my very limited knowledge about some SIMD instructions on ARM), the format doesn't seem to be well suited for it. Maybe someone with a bit more experience can shed some light?\u003c/blockquote\u003e\u003cp\u003e\r\n\tI'd rather take a new image format that's designed around modern SIMD instructions from the start. Then, it can invest these performance gains into more complex filters to end up with better compression at a roughly similar encoding performance. Heck, it can even be slightly slower for all I care. \u003ca href=\"https://github.com/cmuratori/meow_hash\"\u003eSIMD-first design worked great for non-cryptographic hashes\u003c/a\u003e, and we'll see in a minute that it works just as well for image formats.\u003cbr\u003e\r\n\tBut Ember2528 had a different codec in mind anyway. Let's jump right to the polar opposite of the complexity spectrum:\r\n\u003c/p\u003e\u003ch3 id=\"jxl-2025-04-09\"\u003eLossless JPEG XL\u003c/h3\u003e\u003cp\u003e\r\n\tBecause why wouldn't you use the currently best and most popular image format according to actual professionals who know a couple of things about image compression? It's winning benchmarks left and right, and \u003ca href=\"https://cloudinary.com/blog/jpeg-xl-and-the-pareto-front\"\u003eblog posts like these\u003c/a\u003e make it appear as if even version 0.10 of its reference encoder already beats out every other widely used codec. And after it \u003ca href=\"https://issues.chromium.org/issues/40168998#comment85\"\u003eunfairly got removed from Chromium in 2022\u003c/a\u003e, you can't help but root for it. Time to do my small part in bringing its adoption to a level that Google can no longer deny!\r\n\u003c/p\u003e\u003cp\u003e\r\n\tToo bad that the enthusiasm immediately drops after cloning the \u003ca href=\"https://github.com/libjxl/libjxl/\"\u003elibjxl repo\u003c/a\u003e and running a CMake test build. What are all these library dependencies, and why can't I just reduce the build to the lossless encoder? The resulting binaries are way larger than what I'd consider appropriate in relation to game code. 😩\u003cbr\u003e\r\n\tLooking through the repo more thoroughly, however, reveals a very welcome little surprise: \u003ca href=\"https://github.com/libjxl/libjxl/blob/c496c521f99c13b8205c4fc4ff3eb3d652a1d1c3/lib/jxl/encode.cc#L2235-L2296\"\u003eIf a few basic requirements are met\u003c/a\u003e, the fastest lossless speed tier actually uses \u003ca href=\"https://github.com/libjxl/libjxl/blob/0c1aba1d51ed32f61be4de638f075f2b199082d0/lib/jxl/enc_fast_lossless.cc\"\u003ean entirely separate encoder that's implemented in a single source file\u003c/a\u003e and can be used independently from the rest of libjxl. Nice to see that someone thought about simple integration after all! That's exactly what I've hoped to find. Sadly, Linux distributions don't have a separate standalone package for this encoder, but it wouldn't be the only library we'd statically link on Linux.\u003cbr\u003e\r\n\tHaving \u003ca href=\"https://github.com/libjxl/libjxl/blob/0c1aba1d51ed32f61be4de638f075f2b199082d0/lib/jxl/enc_fast_lossless.h#L51-L55\"\u003ea single function as an easy entry point\u003c/a\u003e is always a good sign, too. Those parameters, though… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eOnly accepting pixels in RGBA memory order sure is awkward in a 3D-accelerated world where everything else prefers BGRX, \u003ci\u003eincluding BMP files\u003c/i\u003e. Sure, it doesn't matter for \u003ci\u003eus\u003c/i\u003e because we live in SDL land where we have SIMD-optimized pixel format converters, but \u003ca href=\"https://github.com/libjxl/libjxl/issues/3113#issuecomment-1894741917\"\u003eI don't think you should assume that everyone has these kinds of batteries included\u003c/a\u003e. \u003ca href=\"https://github.com/libjxl/libjxl/issues/2519#issuecomment-1575389359\"\u003e\"Just roll your own\"\u003c/a\u003e isn't a good argument either because you'd \u003ci\u003ewant\u003c/i\u003e pixel format conversions to be SIMD-optimized. We'd all love it if compilers perfectly auto-vectorized such code, but we're not there yet; \u003ca href=\"https://godbolt.org/z/Yx98qo1s3\"\u003eVisual Studio in particular is pretty bad at optimizing naive byte-flipping code\u003c/a\u003e. But writing SIMD code always comes with the same CPU feature detection and alignment boilerplate, and JPEG XL already has all of that in its codebase. Thus, it makes a lot more sense for it to include pixel format converters than forcing that onto every caller. It's API designs like this one that almost necessitate turning SDL into a hard dependency of the cross-platform frontend in the long run.\u003c/li\u003e\r\n\t\u003cli\u003eThe not further documented \u003ccode\u003ebig_endian\u003c/code\u003e parameter is the first indication that a lot of development effort went into aspects we don't care about. You'd think that passing \u003ccode\u003etrue\u003c/code\u003e would cause the \u003ccode\u003ergba\u003c/code\u003e buffer to be interpreted as ABGR, but it's only used to select the per-channel endianness of images with \u003ci\u003e16 bits\u003c/i\u003e per color channel. For 8-bit-per-channel images like the ones we're exclusively dealing with, it silently does nothing.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tAs the FJXL abbreviation implies, this encoder actually started as an independent project that, coincidentally, \u003ca href=\"https://www.lucaversari.it/FJXL_and_FPNGE.pdf\"\u003ewas a direct response to the hype surrounding QOI\u003c/a\u003e. By using AVX2 instructions within the confines of an existing format, it managed to beat QOI in both encoded file sizes \u003ci\u003eand\u003c/i\u003e compression speed for every type of image its developer tested. But it's this competitive focus that brings us to its most questionable implementation decision.\u003cbr\u003e\r\n\tThe good news is that FJXL acknowledges that low-color images exist, are a prime use case for lossless compression, and are best dealt with using JPEG XL's palette features. However, detecting and optimizing that palette takes up a lot of time relative to QOI. If the input image uses more colors than a palette would make sense for, you'd want to fail as early as possible. Slide 11 explains the solution FJXL came up with:\r\n\u003c/p\u003e\u003cblockquote style=\"white-space: unset;\"\u003e\r\n\t\u003cul\u003e\r\n\t\t\u003cli\u003eHash table with 65k possible entries\u003c/li\u003e\r\n\t\t\u003cli\u003eAny collision -\u003e no palette\u003c/li\u003e\r\n\t\t\u003cli\u003e[…]\u003c/li\u003e\r\n\t\u003c/ul\u003e\u003cp\u003e\r\n\t\tOn non-palette-friendly images, this fails quickly (birthday paradox says after ~256 distinct pixels).\r\n\t\u003c/p\u003e\u003cp\u003e\r\n\t\tOn palette images, encoding 1 channel rather than 4 more than compensates the\r\n\t\tcost of detection.\r\n\t\u003c/p\u003e\r\n\u003c/blockquote\u003e\u003cp\u003e\r\n\tWith 10 additional bits and a widely renowned multiplier, the hash function looks leaps and bounds ahead of the one in QOI:\r\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e// has to map 0 to 0\r\nuint16_t pixel_hash(uint32_t p) {\r\n\treturn ((p * 2654435761) \u003e\u003e 16);\r\n}\u003c/pre\u003e\u003cfigcaption\u003e\r\n\tAdapted from \u003ca href=\"https://github.com/libjxl/libjxl/blob/0c1aba1d51ed32f61be4de638f075f2b199082d0/lib/jxl/enc_fast_lossless.cc#L3514-L3523\"\u003ethe original code\u003c/a\u003e.\r\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\r\n\tBut since we're still hashing 32-bit RGBA pixels to 16 bits, we're bound to run into a collision sooner or later. You can certainly think of this hash function as mapping color values to uniformly distributed random numbers and then reason about its efficacy using probability theory, as we saw in the slide above. However, the conclusion drawn in that slide is rather abbreviated and ultimately misleading: The \u003ca href=\"https://en.wikipedia.org/wiki/Birthday_problem\"\u003ebirthday paradox\u003c/a\u003e does \u003ci\u003enot\u003c/i\u003e return a binary success/failure result, but a \u003ci\u003eprobability\u003c/i\u003e. In this case of 256 distinct colors:\r\n\u003c/p\u003e\u003cfigure class=\"formula\"\u003e\r\n\t(1\u0026nbsp;-\u0026nbsp;\u003csup\u003e(\r\n\t\t\u003csup\u003e\r\n\t\t\t65536!\r\n\t\t\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e\r\n\t\t\t(65536 - 256)!\r\n\t\t\u003c/sub\u003e\r\n\t)\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e\r\n\t\t65536\u003csup\u003e256\u003c/sup\u003e\r\n\t\u003c/sub\u003e)\u0026nbsp;≈ 39.27%\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tLet's plug in 191, for no reason whatsoever:\r\n\u003c/p\u003e\u003cfigure class=\"formula\"\u003e\r\n\t(1\u0026nbsp;-\u0026nbsp;\u003csup\u003e(\r\n\t\t\u003csup\u003e\r\n\t\t\t65536!\r\n\t\t\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e\r\n\t\t\t(65536 - 191)!\r\n\t\t\u003c/sub\u003e\r\n\t)\u003c/sup\u003e\u0026nbsp;/\u0026nbsp;\u003csub\u003e\r\n\t\t65536\u003csup\u003e191\u003c/sup\u003e\r\n\t\u003c/sub\u003e)\u0026nbsp;≈ 24.21%\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThat's a smaller probability, but a \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e4\u003c/sub\u003e failure rate would still be way too high for our use case. And sure enough, it actually happens in the main menu, where a single \u003cspan style=\"color: #583732;\"\u003e#583732FF\u003c/span\u003e pixel (or \u003ccode\u003e0xFF323758\u003c/code\u003e in its little-endian representation) collides with #FFFFFFFF:\r\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\r\n\t\u003crec98-child-switcher\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-main_menu.webp?0a7db3d0\"\r\n\t\tdata-title=\"The image\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `main_menu` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-SH01-Main-menu-FJXL-collision-pixel.webp?0098ae41\"\r\n\t\tclass=\"active\"\r\n\t\tdata-title=\"The pixel\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"A 16× zoomed view of the `main_menu` benchmark image, highlighting the single #583732FF pixel that causes the hash collision in FJXL's palette detection code\"\r\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThe resulting 143\u0026nbsp;KiB file immediately tells us how not palettizing such images completely ruins the compression ratio. If this one pixel had any other non-colliding color, FJXL would have compressed it into a still decent 52\u0026nbsp;KiB. Therefore, the slides should have better added a graph of the failure probability, and said something like:\r\n\u003c/p\u003e\u003cblockquote\u003eNot perfect, and likely to misdetect even low-color images with \u0026lt;256 distinct colors as not palette-friendly according to the birthday paradox.\u003c/blockquote\u003e\u003cp\u003e\r\n\tFor our use case of screenshots without an alpha channel, we could work around this whole issue by having a separate non-alpha code path. Detecting the potential palette of an RGBA image within a worst-case time complexity of 𝑂(𝑛) without using hashes requires a (\u003csup\u003e2\u003csup\u003e32\u003c/sup\u003e\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003e)\u0026nbsp;= 512\u0026nbsp;MiB bit array to cover the entire RGBA color space, which is probably too steep of a memory requirement. Removing the alpha channel, however, would shrink this array to a definitely appropriate 2\u0026nbsp;MiB.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tUltimately though, we decided against doing any of that because FJXL by itself is as untunable from the outside as the codec it was inspired by. Ember2528 preferred the opposite: an encoder with multiple effort levels that offer different trade-offs between encoding speed and file size, which would allow faster CPUs to produce the smallest files at still reasonable speeds. So let's look past the bloat, link in the complete libjxl reference encoder, and see how it performs on higher effort levels…\r\n\u003c/p\u003e\u003cp\u003e\r\n\t…um, what is this API? Adapting the \u003ca href=\"https://github.com/libjxl/libjxl/blob/c496c521f99c13b8205c4fc4ff3eb3d652a1d1c3/examples/encode_oneshot.cc\"\u003eexample code\u003c/a\u003e gave me encoding times that are at least 1.5× slower than the \u003ccode\u003ecjxl\u003c/code\u003e command-line encoder, and already hit the 100\u0026nbsp;ms mark at \u003ccode\u003e-e 2\u003c/code\u003e. Even \u003ccode\u003e-e 1\u003c/code\u003e is suddenly much slower than using FJXL in isolation while yielding the same compressed sizes. Also, \u003ca href=\"https://github.com/libjxl/libjxl/blob/c496c521f99c13b8205c4fc4ff3eb3d652a1d1c3/examples/encode_oneshot.cc#L205-L217\"\u003epushing speculative allocation onto the caller\u003c/a\u003e? 🤨 \u003ca href=\"/blog/2024-03-09#libs-2024-03-09\"\u003e📝 stb_vorbis is a bad joke, not a model to be emulated\u003c/a\u003e.\u003cbr\u003e\r\n\tThe compressed file sizes are pretty underwhelming as well. Most of the test cases don't even get close to oxipng at \u003ccode\u003e-e ≤6\u003c/code\u003e while still taking absurdly long to encode within the game. Even at peak effort, it's a mixed bag at best, with both oxipng and JPEG XL \u003ccode\u003e-e 10\u003c/code\u003e massively beating the other in 3 out of 7 cases. And if \u003ci\u003ethat's\u003c/i\u003e the best we can say about this format…\r\n\u003c/p\u003e\u003cp\u003e\r\n\tAll this is echoed by \u003ca href=\"https://github.com/libjxl/libjxl/issues/4150\"\u003ethis recent issue\u003c/a\u003e that points out JPEG XL's inadequacy with an even more retro 16-color example. In the end, the \u003ca href=\"https://github.com/libjxl/libjxl/blob/0c1aba1d51ed32f61be4de638f075f2b199082d0/doc/xl_overview.md#lossless\"\u003edocumentation\u003c/a\u003e said it all along:\r\n\u003c/p\u003e\u003cblockquote\u003eThey are about 60-75% of size of PNG, and smaller than WebP lossless \u003cstrong\u003efor photos.\u003c/strong\u003e\u003c/blockquote\u003e\u003chr id=\"bench-2025-04-09\"\u003e\u003cp\u003e\r\n\tBut there is one widely-used image codec that both perfectly fits Ember2528's priorities \u003ci\u003eand\u003c/i\u003e compresses well on lower effort levels. Let's finally look at the complete benchmark numbers:\r\n\u003c/p\u003e\u003cfigure class=\"ratios benchmark-2025-04-09\"\u003e\r\n\t\u003crec98-child-switcher id=\"numbers-2025-04-09\" data-link=\"images-2025-04-09\"\u003e\r\n\t\t\u003ctable class=\"numbers active\" data-title=\"\u003ccode\u003emain_menu\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003emain_menu\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e146,352\u003c/td\u003e\u003ctd\u003e51,851\u003c/td\u003e\u003ctd\u003e59,453\u003c/td\u003e\u003ctd\u003e45,329\u003c/td\u003e\u003ctd\u003e37,864\u003c/td\u003e\u003ctd\u003e37,276\u003c/td\u003e\u003ctd\u003e36,130\u003c/td\u003e\u003ctd\u003e35,222\u003c/td\u003e\u003ctd\u003e33,793\u003c/td\u003e\u003ctd\u003e31,724\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e54,116\u003c/td\u003e\u003ctd\u003e32,194\u003c/td\u003e\u003ctd\u003e28,112\u003c/td\u003e\u003ctd\u003e27,860\u003c/td\u003e\u003ctd\u003e27,712\u003c/td\u003e\u003ctd\u003e28,272\u003c/td\u003e\u003ctd\u003e28,178\u003c/td\u003e\u003ctd\u003e28,120\u003c/td\u003e\u003ctd\u003e28,684\u003c/td\u003e\u003ctd\u003e27,816\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e272,604\u003c/td\u003e\u003ctd\u003e272,604\u003c/td\u003e\u003ctd\u003e136,220\u003c/td\u003e\u003ctd\u003e131,235\u003c/td\u003e\u003ctd\u003e119,398\u003c/td\u003e\u003ctd\u003e117,525\u003c/td\u003e\u003ctd\u003e111,380\u003c/td\u003e\u003ctd\u003e110,684\u003c/td\u003e\u003ctd\u003e110,543\u003c/td\u003e\u003ctd\u003e109,601\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (8 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e308,278\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;92,034\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;93,884\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;30,702\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003eingame\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003eingame\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e123,606\u003c/td\u003e\u003ctd\u003e102,949\u003c/td\u003e\u003ctd\u003e130,689\u003c/td\u003e\u003ctd\u003e102,944\u003c/td\u003e\u003ctd\u003e84,916\u003c/td\u003e\u003ctd\u003e72,590\u003c/td\u003e\u003ctd\u003e68,302\u003c/td\u003e\u003ctd\u003e49,618\u003c/td\u003e\u003ctd\u003e45,865\u003c/td\u003e\u003ctd\u003e46,997\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e50,678\u003c/td\u003e\u003ctd\u003e49,030\u003c/td\u003e\u003ctd\u003e43,620\u003c/td\u003e\u003ctd\u003e41,760\u003c/td\u003e\u003ctd\u003e40,724\u003c/td\u003e\u003ctd\u003e40,854\u003c/td\u003e\u003ctd\u003e38,608\u003c/td\u003e\u003ctd\u003e37,940\u003c/td\u003e\u003ctd\u003e37,842\u003c/td\u003e\u003ctd\u003e37,138\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e462,703\u003c/td\u003e\u003ctd\u003e462,703\u003c/td\u003e\u003ctd\u003e197,818\u003c/td\u003e\u003ctd\u003e156,007\u003c/td\u003e\u003ctd\u003e141,043\u003c/td\u003e\u003ctd\u003e139,689\u003c/td\u003e\u003ctd\u003e133,399\u003c/td\u003e\u003ctd\u003e132,573\u003c/td\u003e\u003ctd\u003e126,270\u003c/td\u003e\u003ctd\u003e125,379\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (8 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e308,278\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE\u003c/td\u003e\u003ctd colspan=\"10\"\u003e185,842\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e175,949\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;38,409\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e185,398\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e177,456\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e165,620\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003estage6\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003estage6\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e32,204\u003c/td\u003e\u003ctd\u003e24,146\u003c/td\u003e\u003ctd\u003e35,053\u003c/td\u003e\u003ctd\u003e24,599\u003c/td\u003e\u003ctd\u003e19,936\u003c/td\u003e\u003ctd\u003e19,560\u003c/td\u003e\u003ctd\u003e19,336\u003c/td\u003e\u003ctd\u003e18,444\u003c/td\u003e\u003ctd\u003e17,423\u003c/td\u003e\u003ctd\u003e16,183\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e20,856\u003c/td\u003e\u003ctd\u003e19,916\u003c/td\u003e\u003ctd\u003e17,070\u003c/td\u003e\u003ctd\u003e16,524\u003c/td\u003e\u003ctd\u003e16,380\u003c/td\u003e\u003ctd\u003e16,562\u003c/td\u003e\u003ctd\u003e15,488\u003c/td\u003e\u003ctd\u003e15,386\u003c/td\u003e\u003ctd\u003e15,404\u003c/td\u003e\u003ctd\u003e15,124\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e185,676\u003c/td\u003e\u003ctd\u003e185,676\u003c/td\u003e\u003ctd\u003e84,437\u003c/td\u003e\u003ctd\u003e62,354\u003c/td\u003e\u003ctd\u003e57,791\u003c/td\u003e\u003ctd\u003e56,524\u003c/td\u003e\u003ctd\u003e52,956\u003c/td\u003e\u003ctd\u003e52,611\u003c/td\u003e\u003ctd\u003e51,969\u003c/td\u003e\u003ctd\u003e51,795\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (8 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e308,278\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;55,838\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;52,302\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;18,741\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e185,398\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;48,954\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;45,874\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003elaser\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003elaser\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e345,199\u003c/td\u003e\u003ctd\u003e287,279\u003c/td\u003e\u003ctd\u003e301,608\u003c/td\u003e\u003ctd\u003e248,852\u003c/td\u003e\u003ctd\u003e92,463\u003c/td\u003e\u003ctd\u003e85,529\u003c/td\u003e\u003ctd\u003e81,206\u003c/td\u003e\u003ctd\u003e66,811\u003c/td\u003e\u003ctd\u003e61,445\u003c/td\u003e\u003ctd\u003e47,173\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e85,318\u003c/td\u003e\u003ctd\u003e56,724\u003c/td\u003e\u003ctd\u003e51,558\u003c/td\u003e\u003ctd\u003e53,964\u003c/td\u003e\u003ctd\u003e53,492\u003c/td\u003e\u003ctd\u003e53,492\u003c/td\u003e\u003ctd\u003e51,860\u003c/td\u003e\u003ctd\u003e51,460\u003c/td\u003e\u003ctd\u003e51,460\u003c/td\u003e\u003ctd\u003e41,726\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e218,858\u003c/td\u003e\u003ctd\u003e218,858\u003c/td\u003e\u003ctd\u003e122,100\u003c/td\u003e\u003ctd\u003e88,490\u003c/td\u003e\u003ctd\u003e82,675\u003c/td\u003e\u003ctd\u003e81,245\u003c/td\u003e\u003ctd\u003e75,866\u003c/td\u003e\u003ctd\u003e75,395\u003c/td\u003e\u003ctd\u003e75,462\u003c/td\u003e\u003ctd\u003e75,138\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (24 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e921,654\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e290,088\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;61,595\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e553,014\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e280,462\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003elaserbomb\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003elaserbomb\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e332,706\u003c/td\u003e\u003ctd\u003e125,197\u003c/td\u003e\u003ctd\u003e150,436\u003c/td\u003e\u003ctd\u003e128,755\u003c/td\u003e\u003ctd\u003e110,357\u003c/td\u003e\u003ctd\u003e102,891\u003c/td\u003e\u003ctd\u003e99,718\u003c/td\u003e\u003ctd\u003e68,968\u003c/td\u003e\u003ctd\u003e66,975\u003c/td\u003e\u003ctd\u003e64,484\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e129,472\u003c/td\u003e\u003ctd\u003e94,564\u003c/td\u003e\u003ctd\u003e86,538\u003c/td\u003e\u003ctd\u003e64,990\u003c/td\u003e\u003ctd\u003e64,062\u003c/td\u003e\u003ctd\u003e64,062\u003c/td\u003e\u003ctd\u003e60,776\u003c/td\u003e\u003ctd\u003e60,318\u003c/td\u003e\u003ctd\u003e60,318\u003c/td\u003e\u003ctd\u003e59,198\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e313,731\u003c/td\u003e\u003ctd\u003e313,731\u003c/td\u003e\u003ctd\u003e168,388\u003c/td\u003e\u003ctd\u003e114,111\u003c/td\u003e\u003ctd\u003e109,239\u003c/td\u003e\u003ctd\u003e107,121\u003c/td\u003e\u003ctd\u003e104,109\u003c/td\u003e\u003ctd\u003e102,054\u003c/td\u003e\u003ctd\u003e99,106\u003c/td\u003e\u003ctd\u003e99,103\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (24 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e921,654\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e210,496\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;87,286\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e553,014\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e200,002\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003egates\u003c/code\u003e\"\u003e\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003egates\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e208,293\u003c/td\u003e\u003ctd\u003e185,662\u003c/td\u003e\u003ctd\u003e212,615\u003c/td\u003e\u003ctd\u003e172,008\u003c/td\u003e\u003ctd\u003e124,466\u003c/td\u003e\u003ctd\u003e117,509\u003c/td\u003e\u003ctd\u003e113,563\u003c/td\u003e\u003ctd\u003e110,992\u003c/td\u003e\u003ctd\u003e97,454\u003c/td\u003e\u003ctd\u003e91,146\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e124,308\u003c/td\u003e\u003ctd\u003e125,070\u003c/td\u003e\u003ctd\u003e113,896\u003c/td\u003e\u003ctd\u003e102,656\u003c/td\u003e\u003ctd\u003e102,482\u003c/td\u003e\u003ctd\u003e102,482\u003c/td\u003e\u003ctd\u003e95,536\u003c/td\u003e\u003ctd\u003e94,768\u003c/td\u003e\u003ctd\u003e94,768\u003c/td\u003e\u003ctd\u003e57,850\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e306,742\u003c/td\u003e\u003ctd\u003e306,742\u003c/td\u003e\u003ctd\u003e293,874\u003c/td\u003e\u003ctd\u003e293,276\u003c/td\u003e\u003ctd\u003e254,073\u003c/td\u003e\u003ctd\u003e243,953\u003c/td\u003e\u003ctd\u003e243,947\u003c/td\u003e\u003ctd\u003e242,188\u003c/td\u003e\u003ctd\u003e241,943\u003c/td\u003e\u003ctd\u003e241,359\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (24 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e921,654\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e157,705\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;90,545\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e553,014\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u0026ZeroWidthSpace;\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e147,670\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\u003c/table\u003e\u003ctable class=\"numbers\" data-title=\"\u003ccode\u003eseihou\u003c/code\u003e\"\u003e\r\n\t\t\u003cthead\u003e\r\n\t\t\t\u003ctr\u003e\u003cth\u003e\u003ccode\u003eseihou\u003c/code\u003e / Effort\u003c/th\u003e\u003cth\u003e0\u003c/th\u003e\u003cth\u003e1\u003c/th\u003e\u003cth\u003e2\u003c/th\u003e\u003cth\u003e3\u003c/th\u003e\u003cth\u003e4\u003c/th\u003e\u003cth\u003e5\u003c/th\u003e\u003cth\u003e6\u003c/th\u003e\u003cth\u003e7\u003c/th\u003e\u003cth\u003e8\u003c/th\u003e\u003cth\u003e9\u003c/th\u003e\u003c/tr\u003e\r\n\t\t\u003c/thead\u003e\u003ctbody\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eJPEG XL\u003c/td\u003e\u003ctd\u003e6,124\u003c/td\u003e\u003ctd\u003e5,088\u003c/td\u003e\u003ctd\u003e4,732\u003c/td\u003e\u003ctd\u003e4,468\u003c/td\u003e\u003ctd\u003e4,427\u003c/td\u003e\u003ctd\u003e4,416\u003c/td\u003e\u003ctd\u003e4,377\u003c/td\u003e\u003ctd\u003e4,112\u003c/td\u003e\u003ctd\u003e4,016\u003c/td\u003e\u003ctd\u003e4,040\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eWebP\u003c/td\u003e\u003ctd\u003e39,518\u003c/td\u003e\u003ctd\u003e5,904\u003c/td\u003e\u003ctd\u003e5,642\u003c/td\u003e\u003ctd\u003e5,574\u003c/td\u003e\u003ctd\u003e5,500\u003c/td\u003e\u003ctd\u003e5,518\u003c/td\u003e\u003ctd\u003e5,518\u003c/td\u003e\u003ctd\u003e5,504\u003c/td\u003e\u003ctd\u003e5,486\u003c/td\u003e\u003ctd\u003e5,490\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eAVIF\u003c/td\u003e\u003ctd\u003e26,984\u003c/td\u003e\u003ctd\u003e26,984\u003c/td\u003e\u003ctd\u003e25,085\u003c/td\u003e\u003ctd\u003e24,927\u003c/td\u003e\u003ctd\u003e22,582\u003c/td\u003e\u003ctd\u003e21,698\u003c/td\u003e\u003ctd\u003e21,697\u003c/td\u003e\u003ctd\u003e21,627\u003c/td\u003e\u003ctd\u003e21,631\u003c/td\u003e\u003ctd\u003e21,505\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP (8 bpp)\u003c/td\u003e\u003ctd colspan=\"10\"\u003e308,278\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;17,654\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;18,047\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003e\u003ccode\u003eoxipng -o max -Z\u003c/code\u003e\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;\u0026nbsp;5,383\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;23,798\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eBMP/RLE, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;14,144\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\t\u003ctr\u003e\u003ctd\u003eQOI, cropped\u003c/td\u003e\u003ctd colspan=\"10\"\u003e\u0026nbsp;13,371\u003c/td\u003e\u003c/tr\u003e\r\n\t\t\u003c/tbody\u003e\r\n\t\u003c/table\u003e\r\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\r\n\t\u003cfigcaption\u003e\r\n\t\tThe effort value directly corresponds to \u003ccode\u003ecwebp\u003c/code\u003e's \u003ccode\u003e-z\u003c/code\u003e parameter. Add 1 to get \u003ccode\u003ecjxl\u003c/code\u003e's \u003ccode\u003e-e\u003c/code\u003e parameter, and subtract from 10 for \u003ccode\u003eavifenc\u003c/code\u003e's \u003ccode\u003e-s\u003c/code\u003e parameter. \u003cbr\u003e\r\n\t\tI definitely could have surveyed the landscape of PNG encoders more thoroughly, but since Ember2528 prioritized compression ratio over compression speed, there was no need to. oxipng is as good as it gets, but even its strongest and most sluggish setting is still outperformed by regular WebP at \u003ci\u003esome\u003c/i\u003e level, and often as early as \u003ccode\u003e-z\u0026nbsp;2\u003c/code\u003e.\r\n\t\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cfigure class=\"fullres pixelated\"\u003e\r\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\r\n\t\t191 colors. The large areas in black and \u003cspan style=\"background-color: #dde4fa;\"\u003e#DDE4FA\u003c/span\u003e are a great test case for an encoder's RLE capabilities. The menu's half-transparent background is slightly nasty, but should still keep this image well within the range of potential palette-based compression. (Unless you're QOI, of course.)\u003cbr\u003e\r\n\t\tFJXL palette detection collision chance: 24.21%.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t92 colors. Lots of repeated bullet sprites to appropriately represent gameplay, plus a small transparency effect in the Evade gauge that shouldn't complicate compression all too much.\u003cbr\u003e\r\n\t\tFJXL palette detection collision chance: 6.20%.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t96 colors. The wavy clock animation makes Stage 6 \u003ci\u003elook\u003c/i\u003e complex, but we expect encoders to actually have a much easier time on the last three stages due to their backgrounds being mostly black.\u003cbr\u003e\r\n\t\tFJXL palette detection collision chance: 6.72%.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t1219 colors. A simple repeated tile in the background, with a big gradient that is likely to push the color count beyond palette-based algorithms.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t831 colors. Similar to enemy-fired lasers, but with multiple smaller gradients rather than a single big one.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t2326 colors. With a comparatively complex background, bullets, and a big laser, this is probably the most intense test case for lossless compression that this game has to offer.\r\n\t\u003c/div\u003e\u003cdiv\u003e\r\n\t\t40 colors. A small consolation prize for JPEG XL, as the smoothly feathered and blurred colors match the photo-like characteristics this codec was meant to target. Even oxipng gets to barely outperform WebP on this one. Then again, the difference between JPEG XL and WebP is still less than 1.5\u0026nbsp;KiB at most, for an image that doesn't represent the rest of the game.\u003cbr\u003e\r\n\t\tFJXL palette detection collision chance: 1.18%.\r\n\t\u003c/div\u003e\u003c/figcaption\u003e\r\n\t\u003crec98-child-switcher id=\"images-2025-04-09\" data-link=\"numbers-2025-04-09\"\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-main_menu.webp?0a7db3d0\"\r\n\t\tclass=\"active\"\r\n\t\tdata-title=\"\u003ccode\u003emain_menu\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `main_menu` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-ingame.webp?ea3621ff\"\r\n\t\tdata-title=\"\u003ccode\u003eingame\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `ingame` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-stage6.webp?a39ee150\"\r\n\t\tdata-title=\"\u003ccode\u003estage6\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `stage6` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-laser.webp?db764369\"\r\n\t\tdata-title=\"\u003ccode\u003elaser\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `laser` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-laserbomb.webp?cea37e96\"\r\n\t\tdata-title=\"\u003ccode\u003elaserbomb\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `laserbomb` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-gates.webp?908035af\"\r\n\t\tdata-title=\"\u003ccode\u003egates\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `gates` benchmark image.\"\r\n\t\u003e\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-Benchmark-seihou.webp?b6041253\"\r\n\t\tdata-title=\"\u003ccode\u003eseihou\u003c/code\u003e\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"The `seihou` benchmark image.\"\r\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\r\n\u003c/figure\u003e\u003chr id=\"webp-2025-04-09\"\u003e\u003ch3\u003eLossless WebP\u003c/h3\u003e\u003cp\u003e\r\n\tYup, it's \u003ca href=\"/blog/2022-10-31\"\u003e📝 ZMBV beating AV1\u003c/a\u003e all over again. For these kinds of retro game screenshots, JPEG XL is vastly outperformed by its counterpart from the previous generation of widely-used image formats. And not just in terms of compressed file sizes, but also in every single other aspect that matters to us:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eFaster compression times across every effort level? ✅ You bet. Imagine adapting \u003ca href=\"https://github.com/webmproject/libwebp/blob/main/doc/api.md\"\u003eits example code\u003c/a\u003e and actually getting encoding speeds that match the \u003ccode\u003ecwebp\u003c/code\u003e command-line encoder! Which brings us to…\u003c/li\u003e\r\n\t\u003cli\u003eBetter C API? ✅ Check – well-documented and significantly easier to use, and I'm not even using the easiest entry point due to its fixed effort level. libwebp does use a single 32-bit pixel format internally, just like JPEG XL, but what's that, importers for \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/src/webp/encode.h#L465-L486\"\u003eother 32-bit pixel formats\u003c/a\u003e and even \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/extras/extras.h#L46-L53\"\u003epalettized 8-bit\u003c/a\u003e images? Sure, the latter ones are part of the extra code that typically isn't part of Linux distribution packages and it just does \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/extras/extras.c#L124-L151\"\u003ea simple unoptimized loop\u003c/a\u003e. But \u003ci\u003ethat's\u003c/i\u003e how a library communicates that it's the right tool for the job.\u003c/li\u003e\r\n\t\u003cli\u003eLess bloat? ✅ Obviously. The unmodified reference library with all of its SSE and AVX optimizations adds an acceptable 274.5\u0026nbsp;KiB to the statically linked and optimized release binary.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tThat's not to say that libwebp is perfect. Its code makes it very obvious that lossless WebP was designed for 2010-era hardware as the encoder never got optimized for modern CPUs. There was an attempt at optimizing at least the lossy encoder for AVX2, but it was \u003ca href=\"https://chromium-review.googlesource.com/c/webm/libwebp/+/1282969\"\u003eultimately abandoned because it never got fast enough\u003c/a\u003e. Surprisingly, the codebase did \u003ca href=\"https://chromium-review.googlesource.com/c/webm/libwebp/+/6405317\"\u003ereceive new AVX2 code\u003c/a\u003e one week before I released this build, but it only covers the lossless \u003ci\u003edecoder\u003c/i\u003e so far.\u003cbr\u003e\r\n\tAs for concurrency, libwebp does come with support for multi-threaded encoding, and I did activate it for the Shuusou Gyoku integration, but it's only used at effort levels 8 and 9. Also, why is \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/src/webp/encode.h#L311-L324\"\u003e\u003ccode\u003eargb\u003c/code\u003e in this structure\u003c/a\u003e interpreted as native-endian and therefore BGRA memory order, but \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/src/webp/encode.h#L49-L80\"\u003ethese\u003c/a\u003e are interpreted as big-endian?\r\n\u003c/p\u003e\u003cp\u003e\r\n\tBut the main criticism is the same that also applies to JPEG XL: The lossless and lossy modes are lumped into the same repository despite having virtually no code in common, and are \u003ca href=\"https://github.com/webmproject/libwebp/blob/a1ad3f1e379539045dd1604fd91e7a270b8af9d1/src/webp/encode.h#L96\"\u003eselected via a structure field\u003c/a\u003e rather than having unrelated API entry points. This once again makes it very difficult for static linkers to remove all the code on the lossy branches that I never asked for in the first place.\u003cbr\u003e\r\n\tAnd I sure never want to run the lossy encoder under \u003ci\u003eany\u003c/i\u003e circumstance. Lossy WebP deserves all its bad reputation for basically being VP8's intra-frame coding applied to still images. VP8, \u003ca href=\"/blog/2022-10-31\"\u003e📝 if you remember\u003c/a\u003e, is that bad video codec from two generations ago that I'm only serving on this website due to sheer inertia. Applying its enforced YCbCr 4:2:0 chroma subsampling to images does not only make it utterly unsuitable for pixel art, but also even worse than well-compressed JPEG which isn't limited to a single subsampling scheme. If anything in the \u003ccode\u003eGIAN07\u003c/code\u003e process accidentally flips the \u003ci\u003e\"I want lossless\"\u003c/i\u003e flag, I'd rather want the WebP encoder to error out and have the screenshot frontend fall back on BMP than save an image with mutilated colors.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tBut while JPEG XL is a lost cause as far as I'm concerned, I've grown to like lossless WebP too much to leave it trapped within the unfortunate organization of its codebase. Also, there seems to be a lot of untapped potential in the format – really, why does PNG get \u003ca href=\"https://github.com/richgel999/fpng\"\u003eall\u003c/a\u003e \u003ca href=\"https://github.com/lvandeve/lodepng\"\u003ethe\u003c/a\u003e \u003ca href=\"https://github.com/veluca93/fpnge\"\u003eattention\u003c/a\u003e of people writing alternative encoders when lossless WebP is the demonstrably much more capable format?\u003cbr\u003e\r\n\tSo I've decided to \u003ca href=\"https://github.com/nmlgc/libwebp_lossless\"\u003efork libwebp and surgically remove all code related to the lossy encoder\u003c/a\u003e. The statically linked result now only takes up ~100\u0026nbsp;KiB in the Windows build while still being API- and ABI-compatible. Of course, Linux users will still use their distribution's libwebp package with the lossy encoder included, but let's hope that the aforementioned possibility of accidents stays purely theoretical.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tReally though, why have people started to bundle lossless and lossy image codecs under the same format in the first place if their algorithms have nothing in common? It might make sense for Opus where SILK and CELT are different kinds of lossy, but \u003ci\u003elossless\u003c/i\u003e and lossy are two completely different paradigms. The bloat and usability confusion far outweigh any \u003ca href=\"https://www.reddit.com/r/compression/comments/wc61wt/comment/iiaqvbi/\"\u003esituational tricks this might offer\u003c/a\u003e.\r\n\u003c/p\u003e\u003chr id=\"effort-2025-04-09\"\u003e\u003cp\u003e\r\n\tAlright, we found a good format with configurable effort levels, and we're only missing a way for players to \u003ci\u003epick\u003c/i\u003e an effort level. Depending on how they want to use this rapid-fire screenshot feature, almost all of the options make sense in some context:\r\n\u003c/p\u003e\u003col\u003e\r\n\t\u003cli\u003eYou'd like to screenshot a whole section of a stage as fast as possible with the help of the disabled frame rate limiter, and you got plenty of free disk space? You probably want to stick with BMP and compress the screenshots outside of the game, just like how you would have done it without this feature.\u003c/li\u003e\r\n\t\u003cli\u003eA slight slowdown is OK or maybe even welcome for providing additional feedback that you're actually taking screenshots? Pick one of WebP's higher effort values that certainly take longer than 16\u0026nbsp;ms to encode, but are still reasonably fast and won't turn the game into a \u0026lt;2-FPS slideshow.\u003c/li\u003e\r\n\t\u003cli\u003eWant the lowest file size that your system can encode while staying at 62.5\u0026nbsp;FPS? Well, how fast \u003ci\u003eis\u003c/i\u003e your system? And not just the CPU – maybe your system is actually bottlenecked by I/O and writing a large uncompressed BMP file takes much longer than encoding it into WebP and writing the resulting smaller file.\u003c/li\u003e\r\n\u003c/ol\u003e\u003cp\u003e\r\n\tThe latter two use cases would be covered by automatic detection of the maximum effort value that encodes within a given number of frames. The problem, however, is that encoding times are always relative to the complexity of the image. Once we're in-game and have lots of bullets and lasers, any choice that might have been appropriate for the main menu might suddenly start dropping frames after all. Thus, we can't solve this with an upfront benchmark, but have to dynamically adapt to the complexity of the current game scene. But then the whole idea falls apart as we can't possibly treat the configurable \u003cvar\u003eallowed screenshot time\u003c/var\u003e as a hard limit. To figure out whether it's safe to raise the effort level again, there's no way around  periodically exceeding that limit and thus dropping more frames after all.\u003cbr\u003e\r\n\tThe ideal solution would involve deep hooks into the WebP encoder that could dynamically adjust the compression algorithms depending on the remaining time in the current frame. An image compressor with \u003ca href=\"https://en.wikipedia.org/wiki/Real-time_computing\"\u003ereal-time guarantees\u003c/a\u003e… sure sounds like an interesting research project.\r\n\u003c/p\u003e\u003cp\u003e\r\n\tIn the end, letting players choose a fixed format and effort level remains the best option. However, they can only make an informed choice if they know the performance of all options relative to each other. And that's how we arrive at this new submenu:\r\n\u003c/p\u003e\u003cfigure id=\"perf-2025-04-09\" class=\"fullres pixelated bglayer main-menu-2025-04-09\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-SH01-Screenshot-times-8400T.webp?5e826959\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"32-bit Shuusou Gyoku screenshot encoding times on an Intel Core i5 8400T from 2018, which is just slightly too slow to guarantee real-time encoding at -z 0 without frame drops\"\r\n\t\u003e\u003cfigcaption\u003eThese measurements start before retrieving the framebuffer's pixels, and end after the file writing syscalls. If you save to a reasonably fast and write-cached storage medium, these syscalls are unlikely to have a big impact. Thus, the BMP times almost purely represent the fixed cost of the \u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e call.\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThese specific numbers I got on my now almost 7-year-old Intel Core i5 8400T are very peculiar. \u003ccode\u003e-z\u0026nbsp;0\u003c/code\u003e gets quite close to the 16\u0026nbsp;ms we have per frame, but would still be too slow to reliably compress every gameplay situation without dropping frames. \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/84\"\u003eA 64-bit build\u003c/a\u003e would speed up \u003ccode\u003e-z\u0026nbsp;0\u003c/code\u003e by 10%, \u003ccode\u003e-z\u0026nbsp;2\u003c/code\u003e through \u003ccode\u003e-z\u0026nbsp;7\u003c/code\u003e by 25%, \u003ccode\u003e-z\u0026nbsp;8\u003c/code\u003e by 210% (!), and \u003ccode\u003e-z\u0026nbsp;9\u003c/code\u003e by 60%. Linux users already enjoy these higher speeds, and the Windows build is just a few compiler settings away from matching them. \u003ca href=\"/blog/2024-10-22#palettized-2024-10-22\"\u003e📝 Last time, the bitness argument was a lot more balanced\u003c/a\u003e, but WebP encoding performance presents the first compelling reason for going 64-bit.\u003cbr\u003e\r\n\tOr we could always \u003ca href=\"https://github.com/nmlgc/ssg/issues/86\"\u003ego multi-threaded\u003c/a\u003e, which already is a much more popular idea within the \u003ci\u003eSeihou development\u003c/i\u003e Discord group.\u003cbr\u003e\r\n\tOr I could investigate PNG after all to find out how exactly its encoding speed compares to WebP… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\r\n\u003c/p\u003e\u003cp\u003e\r\n\tBut then, Ember2528 posted the encoding times he got on his new Ryzen 9 9950X3D:\r\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated bglayer main-menu-2025-04-09\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-SH01-Screenshot-times-9950X3D.webp?b39880ed\"\r\n\t\twidth=\"640\"\r\n\t\talt=\"Shuusou Gyoku screenshot encoding times on a Ryzen 9 9950X3D, showcasing how this CPU can compress at least main menu screenshots in real-time using WebP ≤z7, without dropping frames\"\r\n\t\u003e\r\n\t\u003cfigcaption\u003e…yeah, I probably won't get funding for performance tuning.\u003c/figcaption\u003e\r\n\u003c/figure\u003e\u003chr id=\"tag-2025-04-09\"\u003e\u003cp\u003e\r\n\tFinally, you probably already noticed another small change in this build: The ReC98 push ID is now shown in the bottom-right corner of the title screen image, just below the original game version number. This was the one part of replay preparations that I wanted to get in sooner rather than later. Since the game binary and the data files can be updated or modded independently from each other, I'm going to tag future replays with both of their respective versions to guarantee reproducibility. Of course, newer builds should never introduce bugs that affect gameplay and desynchronize existing replays. But if they ever do, the included push ID allows hosting sites to remove any replays recorded on such a broken build from the official competition tier associated with a specific data file version.\u003cbr\u003e\r\n\tAs for rendering the push ID, it should obviously look similar to the \u003ci\u003e\u003csmall\u003eVERSION\u003c/small\u003e 1.005\u003c/i\u003e text above. We can find these glyphs in \u003ccode\u003eGRAPH.DAT\u003c/code\u003e file #0, but this particular text is actually baked into the main menu's background image, which explains why the decimal point glyph isn't part of that data file. The glyphs for 0-9 are also used in-game for the score popups, but the A-Z glyphs remain \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e – so unused, in fact, that pbg didn't even leave any reference to them in the source code:\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 416px;\"\u003e\r\n\t\u003cimg src=\"/blog/static/2025-04-09-SH01-GRAPH.DAT-5x5-A-to-Z.webp?4d021c97\" width=\"416\" alt=\"The unused 5×5 uppercase gradient font in GRAPH.DAT file #0\"\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tThis means that the game provides us with all the glyphs we would need to display the ReC98 push ID. However:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003eThe 0-9 glyphs have a size of 5×7 and would stick out a bit too much against a capital P rendered as a smaller 5×5 glyph.\u003c/li\u003e\r\n\t\u003cli\u003eIn WIP builds, the build ID should also include the Git commit, which traditionally uses small letters. Surrounding the commit info with (brackets) would also be nice.\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tSo, all the glyphs next to the \u003ci\u003e\u003csmall\u003eBUILD\u003c/small\u003e\u003c/i\u003e label actually come from the TrueType text renderer. The non-slashed zeroes immediately give this away, but exactly emulating the color gradient of the 0-9 glyphs makes MS Gothic blend in very well regardless:\r\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 320px;\"\u003e\r\n\t\u003cimg\r\n\t\tsrc=\"/blog/static/2025-04-09-SH01-ReC98-build-tag.webp?7be05d7e\"\r\n\t\twidth=\"320\"\r\n\t\talt=\"Screenshot of the bottom-right corner of Shuusou Gyoku's title screen in the P0309 build, showing the new ReC98 build tag below the version number baked into the original title screen image\"\r\n\t\u003e\r\n\u003c/figure\u003e\u003cp\u003e\r\n\tAnd that's all I've got for these very packed three pushes! In exchange, I'll reserve the next Shuusou Gyoku push for another round of maintenance and forward compatibility.\u003cbr\u003e\r\n\tThe new builds:\r\n\u003c/p\u003e\u003cul\u003e\r\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0309\"\u003e\r\n\t\u003cimg src=\"/static/emoji-sh01n-032.png?38cbcd51\" alt=\":sh01n:\" width=\"24\" height=\"24\" \n\t\t\tsrcset=\"/static/emoji-sh01n-016.png?b4ffeada 0.66x, /static/emoji-sh01n-032.png?38cbcd51 1.33x, /static/emoji-sh01n-048.png?67a1addc 2x, /static/emoji-sh01n-128.png?96524b5d 5.33x\"\u003e Shuusou Gyoku P0309 Windows build\u003c/a\u003e\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://aur.archlinux.org/packages/seihou-shuusou-gyoku\"\u003eShuusou Gyoku on the AUR\u003c/a\u003e\u003c/li\u003e\r\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://flathub.org/apps/net.nmlgc.rec98.sh01\"\u003eShuusou Gyoku on Flathub\u003c/a\u003e\u003c/li\u003e\r\n\u003c/ul\u003e\u003cp\u003e\r\n\tNext up: The long-awaited Windows 98 backport of our Shuusou Gyoku build! This has been in development for quite a while, so this should now be a matter of days rather than weeks.\r\n\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-04-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-02-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-04-09T03:03:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-02-24",
      "url": "https://rec98.nmlgc.net/blog/2025-02-24",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-04-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-01-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-02-24\"\u003e\u003ctime datetime=\"2025-02-24T23:48:53Z\"\u003e2025-02-24 23:48\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0304\"\u003eP0304\u003c/a\u003e\n\t\t\tTH02 RE (Stage / (mid)boss variables) + Decompilation (Bullets, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/71f4810...96ada76\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0305\"\u003eP0305\u003c/a\u003e\n\t\t\tTH02 decompilation (Bullets, part 2/2 + Sparks, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/96ada76...3c710f9\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0306\"\u003eP0306\u003c/a\u003e\n\t\t\tTH02 decompilation (Player, part 1/2: Update/render functions + Miss animation) + Random TH04/TH05 finalization\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3c710f9...165f090\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga, iruleatgames, nrook, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cstyle\u003e\n\t#miko-2025-02-24 img {\n\t\tbackground-image: url('/blog/static/2025-02-24-TH02-MIKO.BFT-Checkerboard.png?94dd0ab8');\n\t\tbackground-size: cover;\n\t}\n\n\t#hb-2025-02-24 img {\n\t\tbackground-image: url('/blog/static/2025-02-24-TH02-Hitbox-Reimu.png?5302caf5');\n\t\tbackground-size: cover;\n\t}\n\n\t#asymmetry-2025-02-24 img {\n\t\tbackground-image: url('/blog/static/2025-02-24-TH02-2-spread-asymmetry.png?3c99db63');\n\t\tbackground-size: cover;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tSometimes, the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e community will come up with the most outlandish theories before they even begin to consider the idea that certain safespots might not be intentional and only work by accident to begin with. Want more details? Read on…\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#overview-2025-02-24\"\u003eOverview of TH02's bullet system\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#order-2025-02-24\"\u003eThe TH02 Boss Decompilation Order Announcement\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#midbossx-2025-02-24\"\u003eAn Extra Stage midboss?\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#hitbox-2025-02-24\"\u003eHitboxes\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sar-2025-02-24\"\u003eThe fundamental inaccuracy in PC-98 Touhou trigonometry\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#myth-2025-02-24\"\u003eThe myth of death-induced hitbox shifting\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"overview-2025-02-24\"\u003e\u003cp\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo, TH02's bullet system! At a high level, it marks an interesting transitional point: It's still very much based on TH01's design with its predefined static or aimed spreads, but also introduces a few features that would later return in TH04 and TH05. By transplanting the TH01 system into a double-buffered environment, ZUN eliminated the \u003ca href=\"/blog/2020-07-12\"\u003e📝 worst\u003c/a\u003e \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 unblitting-related parts\u003c/a\u003e that plagued TH01, ending up with the simplest and cleanest implementation of bullets I've seen so far. That's not to say it's \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e – far from it – but it also hasn't reached the messy levels that TH04 and especially TH05 would bring later. Of course, there's still TH03's system left to be done until I can say for sure, but TH02's is a pretty strong contender.\n\u003c/p\u003e\u003cp\u003e\n\tThe more detailed overview of the system:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003eTH02 introduces the distinction between the white 8×8 \u003ci\u003epellets\u003c/i\u003e and the 16×16 sprite bullets that TH04 and TH05 would later expand upon.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe game has a single cap of 150 that is shared among both 8×8 and 16×16 bullets, unlike TH04 and TH05 where the cap is split for optimization reasons.\u003cbr\u003e\n\tIn \u003ccode\u003e封魔録.TXT\u003c/code\u003e, ZUN claims that TH02 could even compete with DoDonPachi in terms of bullet amounts:\u003c/p\u003e\n\t\u003cblockquote\u003e怒首領蜂もびっくりな判定の小ささ、\u003cstrong\u003e弾の量\u003c/strong\u003e。\u003c/blockquote\u003e\n\tCan it really, though? DoDonPachi spawns decidedly more bullets than TH02 throughout all of the game, and \u003ca href=\"https://youtu.be/7A04cCcw3k0?t=3359\"\u003ethis pattern\u003c/a\u003e definitely exceeds 150 bullets. Hence, we can immediately debunk this claim as marketing hyperbole rather than a factual statement about the game. It would be nice to have a specific bullet cap number for DoDonPachi as well, but I can't find a decompilation project or annotated disassembly. Nor for any other CAVE game either, for that matter… 👀\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eTH01's decay and delay cloud effects were removed for TH02. Slightly unfortunate as it leaves bullets completely without any sprite effect, but hey, less code surface to mess up!\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eAll bullets lose 0.625 pixels of per-frame speed on Easy and gain an extra 0.75 pixels of per-frame speed on Lunatic. Each bullet is clamped to a minimum speed of at least 1 pixel per frame; on Easy, the game also filters every second bullet that would have been slower. This mechanism mainly kicks in with the blob enemies at minimum rank during Stage 4.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eTH02 sticks with the fixed 2-, 3-, 4-, and 5-way spreads that TH01 introduced, but adds a third delta angle variant on top of TH01's two \u003ci\u003e\"narrow\"\u003c/i\u003e and \u003ci\u003e\"wide\"\u003c/i\u003e ones. 2-spreads even get a fourth \"ultrawide\" angle, which Evil Eye Σ uses in the pellet corridor pattern during its last phase.\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Bullet-spreads-count-order.webp?c45f6334\" preload=\"none\" controls data-title=\"Count order\" loop data-active width=\"384\" height=\"192\" data-fps=\"56.423132\" data-frame-count=\"523\" style=\"aspect-ratio: 384 / 192\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Bullet-spreads-count-order.avi?4839f028\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Bullet-spreads-count-order.webm?45726891\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Bullet-spreads-count-order.webm?bb8e7023\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Bullet-spreads-count-order.webm?d352828b\" type=\"video/webm\"\u003eVideo showcasing all predefined spread groups supported in TH02's bullet system, sorted in group-count-major and delta-angle-minor order. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Bullet-spreads-count-order.avi?4839f028\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"160\" data-title=\"3\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"280\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"400\" data-title=\"5\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Bullet-spreads-delta-angle-order.webp?c45f6334\" preload=\"none\" controls data-title=\"Delta angle order\" loop width=\"384\" height=\"192\" data-fps=\"56.423132\" data-frame-count=\"520\" style=\"aspect-ratio: 384 / 192\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Bullet-spreads-delta-angle-order.avi?8b00820e\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Bullet-spreads-delta-angle-order.webm?579b04c2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Bullet-spreads-delta-angle-order.webm?2f5c1100\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Bullet-spreads-delta-angle-order.webm?6db267de\" type=\"video/webm\"\u003eVideo showcasing all predefined spread groups supported in TH02's bullet system, sorted in delta-angle-major and group-count-minor order. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Bullet-spreads-delta-angle-order.avi?8b00820e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Narrow\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"160\" data-title=\"Medium\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"320\" data-title=\"Wide\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"480\" data-title=\"Ultrawide\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eTH02 also adds predefined 4-, 8-, 16-, and 32-ring groups, all of which are used by bosses.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe game does not yet offer predefined stack groups, but has an auto-stacking system that automatically turns \u003ci\u003eevery\u003c/i\u003e spawned group into a potential 2-stack on Hard and Lunatic. This system forms the main way in which these difficulties differ from the easier ones, and is exactly why going from Normal to Hard roughly doubles the number of bullets fired. On Hard, the second bullet in each stack moves at half the speed of the primary bullet, while Lunatic adds another 0.5 pixels per frame onto that halved speed.\u003cbr\u003e\n\tThe game also has a function to apply a further multiplier on top of the difficulty-specific stack count, but only uses it to temporarily disable stacking during three patterns, one of them used by the Five Magic Stones and two of them used by Mima.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eJust like all other games, TH02 offers a variety of special bullet motion types. For some reason, ZUN limited these to single 16×16 bullets in TH02; they are not supported for either 8×8 pellets or any of the multi-pellet groups. There is no \u003ci\u003etechnical\u003c/i\u003e reason for this, so ZUN likely did this as a deliberate game design choice. The upside is that you as a player can be certain that every 8×8 pellet moves in a straight line, which may or may not help reading patterns.\u003c/p\u003e\n\t\u003cul\u003e\n\t\t\u003cli\u003e\u003ci\u003eChase\u003c/i\u003e bullets adjust their X/Y velocity by a configurable amount on every frame relative to the player's location. These are exclusively used by the \u003cspan lang=\"ja\"\u003e呪\u003c/span\u003e bullets fired by the Stage 2 midboss.\u003c/li\u003e\n\t\t\u003cli\u003e\u003ci\u003eHoming\u003c/i\u003e bullets work in a very similar way, re-aiming at the player more properly for a customizable number of frames after a bullet was spawned. These are completely unused.\u003c/li\u003e\n\t\t\u003cli\u003e\u003ci\u003eDecelerating\u003c/i\u003e bullets reduce their speed to 0 by halving their velocity every 8 frames, and then turn and repeat this process a fixed number of times. In TH02, this movement type is only used in a symmetric green-ball pattern used by the eastern and western Magic Stones, but it would become really popular later on, showing up in 6 of TH04's midboss and/or boss patterns and 9 of TH05's.\u003c/li\u003e\n\t\t\u003cli\u003e\u003ci\u003eGravity\u003c/i\u003e bullets add a customizable acceleration factor to their Y position on every frame. Another movement type exclusive to a single green-ball pattern by the northern Magic Stone, and interestingly special-cased to bypass any difficulty- or rank-based speed tuning.\u003c/li\u003e\n\t\t\u003cli\u003e\u003ci\u003eDrift\u003c/i\u003e bullets either add a remote-controlled angle and speed delta value to a bullet's angle and speed on every frame, or use that remote-controlled angle to chase toward the player using the same algorithm as the \u003cspan lang=\"ja\"\u003e呪\u003c/span\u003e bullets. These two types are criminally underutilized and could have created some widely inventive patterns that you wouldn't have expected out of the first PC-98 Touhou shmup. Instead, they're only used for two of Marisa's rotating star patterns.\u003c/li\u003e\n\t\t\u003cli\u003eAnd finally, of course, we have bullets that bounce and flip their direction near the edge of the playfield. In this game, the bounce edges actually lie 8 pixels inside the playfield:\u003cfigure style=\"width: 384px\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Bounce-margin.webp?60663551\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"213\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Bounce-margin.avi?447f2dbf\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Bounce-margin.webm?ef9d9a63\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Bounce-margin.webm?43c3a49b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Bounce-margin.webm?919bc75e\" type=\"video/webm\"\u003eVideo of TH02's margin for bouncing bullets, 8 pixels away from every edge of the playfield, demonstrated with Meira's billiard balls. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Bounce-margin.avi?447f2dbf\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\t\u003cfigcaption\u003eThe velocity flip only happens on the frame in which a bullet enters the red bounce margin zone. So, faster bullets might still travel a good deal toward the actual edge of the playfield before getting flipped.\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\n\t\tThis type is not only used by Meira's and Evil Eye Σ's red and purple billiard ball bullets, but also by some star bullet patterns during the Mima fight.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003ePellet rendering is batched! For the first time, ZUN preserves the GRCG state for successively blitted pellets, avoiding the extra \u003e168 cycles per pellet that master.lib's \u003ccode\u003egrcg_setcolor()\u003c/code\u003e and \u003ccode\u003egrcg_off()\u003c/code\u003e would cost on a 486. The caveat, however, lies in the words \u003ci\u003esuccessively blitted\u003c/i\u003e. Without an architectural split between pellets and sprite bullets, the rendering code ends up looking like this:\u003c/p\u003e\n\t\u003cfigure\u003e\u003cpre\u003efor(const auto\u0026 bullet : bullets) {\n\t// (Update code…)\n\n\tif(bullet.is_pellet) {\n\t\tif(not_rendering_pellets) {\n\t\t\tgrcg_setcolor(GC_RMW, V_WHITE);\n\t\t\tnot_rendering_pellets = false;\n\t\t}\n\t\tblit_hardcoded_pellet_sprite_using_grcg(bullet);\n\t} else {\n\t\tgrcg_off();\n\t\tsuper_roll_put_tiny(bullet.left, bullet.top, bullet.patnum);\n\t\tnot_rendering_pellets = true;\n\t}\n}\u003c/pre\u003e\u003c/figure\u003e\n\t\u003cp\u003eWhile this definitely is suboptimal once you start mixing the two size types, it's not too bad in context. The actual bullet scripts in TH02 mostly stick to one of the two sprite types, and once the script switches from one to the other, the old and new bullets will occupy mostly contiguous areas of the bullet array anyway. The game doesn't actually mix 8×8 and 16×16 bullets within the same pattern until literally the last pattern of Mima's second form.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eThe four other ZUN quirks in the system are all related to clipping and aim point calculations. ZUN tries very hard to use constants that are supposed to work for both 8×8 and 16×16 bullets, but they never perfectly fit either of the two. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"order-2025-02-24\"\u003e\u003cp\u003e\n\tTo find out where all these bullet types are used, I of course had to label all the individual pattern functions and assign them to their (mid)boss owners. As a side effect, we now also know the preferred boss decompilation order for this game!\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eMarisa\u003c/li\u003e\n\t\u003cli\u003eMima\u003c/li\u003e\n\t\u003cli\u003eEvil Eye Σ\u003c/li\u003e\n\t\u003cli\u003eMeira\u003c/li\u003e\n\t\u003cli\u003eRika\u003c/li\u003e\n\t\u003cli\u003e5 Magic Stones\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tQuite a satisfying order, if I may say so myself – burning off the big fireworks right in the beginning, getting slightly more unexciting later on, but then ending on arguably the best Touhou character ever conceived. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr id=\"midbossx-2025-02-24\"\u003e\n\tEach of these decompilations will be preceded by the stage's respective midboss. This includes the Extra Stage – you might not \u003ci\u003ethink\u003c/i\u003e that this stage has a midboss, but it technically does, in the form of this combination of patterns:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss.webp?c4633b77\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"420\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss.avi?f055e4d2\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss.webm?9d40ebe4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss.webm?d07e8ac3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss.webm?0ee59649\" type=\"video/webm\"\u003eVideo of the TH02 Extra Stage midboss. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss.avi?f055e4d2\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003cfigcaption\u003eLasting exactly these 420 frames.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThere's nothing in TH02's code that mandates midbosses to have sprite-like entities or even something like an HP bar. Instead, the code-level definition of a \u003ci\u003emidboss\u003c/i\u003e is all about these properties:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt assigns control functions to the same function pointers that the other stages use for their midbosses.\u003c/li\u003e\n\t\u003cli\u003eThese functions are activated at a fixed, specific point throughout the stage.\u003c/li\u003e\n\t\u003cli\u003eRegular stage enemy spawns are deactivated until these control functions signal completion.\u003c/li\u003e\n\t\u003cli\u003eIf a pattern manipulates stage tiles, it can only be part of a boss or midboss with custom C code, as this is not supported for regular stage enemy scripts.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tStage 5, on the other hand, indeed doesn't have anything that can be interpreted as a midboss.\n\u003c/p\u003e\u003chr id=\"hitbox-2025-02-24\"\u003e\u003cp\u003e\n\tFinally, and probably most importantly, hitboxes! The raw decompilation of TH02's bullet collision detection code looks like this:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e// 8×8 pellets\n(pellet_left \u0026gt;= (player_left +  7)) \u0026\u0026\n(pellet_left \u0026lt;  (player_left + 17)) \u0026\u0026\n(pellet_top  \u0026gt;= (player_top  + 12)) \u0026\u0026\n(pellet_top  \u0026lt;= (player_top  + 22))\n\n// 16×16 bullets\n(bullet_left \u0026gt;= (player_left -  3)) \u0026\u0026\n(bullet_left \u0026lt;  (player_left + 19)) \u0026\u0026\n(bullet_top  \u0026gt;= (player_top  +  4)) \u0026\u0026\n(bullet_top  \u0026lt;= (player_top  + 24))\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHowever, if you aren't deeply familiar with the sizes of all involved sprites, these top-left positions slightly obscure the actual position of the hitbox. That top-left point might also not be where you think it is:\n\u003c/p\u003e\u003cfigure id=\"miko-2025-02-24\" class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-MIKO.BFT-0.png?c7d67825\"\n\t\tdata-title=\"Standing still\"\n\t\talt=\"Sprite #0 of TH02's MIKO.BFT\"\n\t\twidth=\"320\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-MIKO.BFT-2.png?6497c9fc\" data-title=\"Moving left\" alt=\"Sprite #2 of TH02's MIKO.BFT\" width=\"320\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-MIKO.BFT-3.png?4f5b8b73\" data-title=\"Moving right\" alt=\"Sprite #3 of TH02's MIKO.BFT\" width=\"320\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eIt's the \u003cspan style=\"color: red;\"\u003ered\u003c/span\u003e point.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo let's transform these checks to a more useful comparison of the respective center points against each other, and also fix that inconsistency of the right coordinates being compared with \u003ccode\u003e\u0026lt;\u003c/code\u003e instead of \u003ccode\u003e\u0026lt;=\u003c/code\u003e like the other values:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e// 8×8 pellets\n(pellet_center_x \u0026gt;= (player_center_x - 5)) \u0026\u0026\n(pellet_center_x \u0026lt;= (player_center_x + 4)) \u0026\u0026\n(pellet_center_y \u0026gt;= (player_center_y - 8)) \u0026\u0026\n(pellet_center_y \u0026lt;= (player_center_y + 2))\n\n// 16×16 bullets\n(bullet_center_x \u0026gt;= (player_center_x - 11)) \u0026\u0026\n(bullet_center_x \u0026lt;= (player_center_x + 10)) \u0026\u0026\n(bullet_center_y \u0026gt;= (player_center_y - 12)) \u0026\u0026\n(bullet_center_y \u0026lt;= (player_center_y +  8))\u003c/pre\u003e\n\t\u003cfigcaption\u003eNow also revealing the horizontal asymmetry that ZUN's code was sneakily hiding.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTH02 has only 5 different bullet shapes and no directional or vector bullets, so we can exactly visualize all of them:\n\u003c/p\u003e\u003cfigure id=\"hb-2025-02-24\" class=\"pixelated\" style=\"width: 384px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Hitbox-Bullet-Pellet.png?b8e18d9f\"\n\t\tdata-title=\"Pellet\"\n\t\talt=\"Hitbox of TH02's 8×8 pellets\"\n\t\tclass=\"active\"\n\t\twidth=\"384\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Hitbox-Bullet-Ball.png?dedc1a8a\" data-title=\"Ball\" alt=\"Hitbox of TH02's 16×16 ball bullets\" width=\"384\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Hitbox-Bullet-%e5%91%aa.png?15ea7096\" data-title=\"呪\" alt=\"Hitbox of TH02's 呪 bullets\" width=\"384\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Hitbox-Bullet-Billiard.png?c8c099fc\" data-title=\"Billiard\" alt=\"Hitbox of TH02's billiard bullets\" width=\"384\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Hitbox-Bullet-Star.png?b9d92433\" data-title=\"Star\" alt=\"Hitbox of TH02's star bullets\" width=\"384\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\u003ca href=\"/blog/2022-06-17\"\u003e📝 As\u003c/a\u003e \u003ca href=\"/blog/2022-07-10\"\u003e📝 usual\u003c/a\u003e, a bullet sprite has to be fully surrounded by the blue box for a hit to be registered.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tYup. Quite asymmetric indeed, and probably surprising no one.\n\u003c/p\u003e\u003chr id=\"sar-2025-02-24\"\u003e\u003cp\u003e\n\tWhile experimenting with the various hardcoded group types, I stumbled over a quite surprising quirk that you might have already noticed in the spread showcase video further above. For some reason, none of these spreads are perfectly symmetric, what the…?\n\u003c/p\u003e\u003cfigure id=\"asymmetry-2025-02-24\" class=\"pixelated\" style=\"width: 768px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-2-spread-asymmetry.png?3c99db63\"\n\t\tdata-title=\"Pattern\"\n\t\talt=\"A 2-spread pattern using a base angle of 0x40, TH02's hardcoded medium spread angle, and spawned at the center of the playfield to trap the player at its spawn point\"\n\t\tclass=\"active\"\n\t\twidth=\"768\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-2-spread-asymmetry-left.svg?3201cc66\"\n\t\tdata-title=\"Asymmetry (left)\"\n\t\talt=\"Visualization of the asymmetry in this 2-spread pattern if the right lane moved at the correct angle; the cyan area shows the symmetric triangle the pattern is expected to form, and the red area shows the inaccurate extra amount of space covered by the left lane\"\n\t\twidth=\"768\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-2-spread-asymmetry-right.svg?90635cbc\"\n\t\tdata-title=\"Asymmetry (right)\"\n\t\talt=\"Visualization of the asymmetry in this 2-spread pattern if the left lane moved at the correct angle; the cyan area shows the asymmetric triangle being formed by the pattern as it is, and the red area shows the extra amount of space missing from the right lane to make the pattern actually symmetric\"\n\t\twidth=\"768\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eBy the time the bullets have reached the bottom of the playfield, the inaccuracy has compounded so much that the right lane ends up 6 pixels closer to the player's center position than the left lane. Depending on which of the two lanes actually gets the correct angle, this either means that the left lane is moving too far (2️⃣) or that the right lane is not moving far enough (3️⃣).\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis is very weird because the angles that go into the velocity calculations are demonstrably correct. You'd therefore get this asymmetry for not only the hardcoded spreads, but also for code that does its own angle calculations and spawns each bullet manually. It's not something that can arise from the other known issue of \u003ca href=\"/blog/2022-03-05\"\u003e📝 Q12.4 quantization\u003c/a\u003e either, because that would affect all parts of a pattern equally.\n\t\u003cbr\u003e\n\tInstead, the inaccuracy originates in the conversion from the polar coordinates of angles and speeds into the per-frame X/Y pixel velocities that the game uses for actual movement. The integer math algorithm that ZUN uses here is pretty much the single most fundamental piece of code shared by all 5 games:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e// Using \u003ca href=\"/blog/2022-03-05\"\u003e📝 typical 8-bit angles\u003c/a\u003e.\nint16_t polar_x(int16_t center, int16_t radius, uint8_t angle)\n{\n\t// Ensure that the multiplication below doesn't overflow\n\tint32_t radius32 = radius;\n\n\t// Get the cosine value from master.lib's lookup table, which scales the\n\t// real-number range of [-1; +1] to the integer range of [-256; +256].\n\tint16_t cosine = CosTable8[angle];\n\n\t// The multiplication will include master.lib's 256× scaling factor, so\n\t// divide the result to bring it within the intended radius.\n\treturn (((radius * cosine) \u0026gt;\u0026gt; 8) + center);\n}\u003c/pre\u003e\u003cfigcaption\u003e\n\tThis exact algorithm is even recommended in the master.lib manual.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\u003c/p\u003e\u003cp\u003e\n\tThe pattern above uses TH02's medium delta angle for 2-spreads and moves at a Q12.4 subpixel speed of 2.5, which corresponds to a radius of 40 in the context of polar coordinate calculation. Let's step through it:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eAngle\u003c/th\u003e\n\t\t\t\u003cth\u003eCosine\u003c/th\u003e\n\t\t\t\u003cth\u003eMultiplied\u003c/th\u003e\n\t\t\t\u003cth\u003eIn hex\u003c/th\u003e\n\t\t\t\u003cth\u003eShift result\u003c/th\u003e\n\t\t\t\u003cth\u003eIn decimal\u003c/th\u003e\n\t\t\t\u003cth\u003eIn Q12.4\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody style=\"font-family: monospace;\"\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e(0x40 - 6)\u003c/th\u003e\n\t\t\t\u003ctd\u003e38\u003c/td\u003e\n\t\t\t\u003ctd\u003e1520\u003c/td\u003e\n\t\t\t\u003ctd\u003e000005F0\u003c/td\u003e\n\t\t\t\u003ctd\u003e00000005\u003c/td\u003e\n\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003ctd\u003e0.3125\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e(0x40 + 6)\u003c/th\u003e\n\t\t\t\u003ctd\u003e-38\u003c/td\u003e\n\t\t\t\u003ctd\u003e-1520\u003c/td\u003e\n\t\t\t\u003ctd\u003eFFFFFA10\u003c/td\u003e\n\t\t\t\u003ctd\u003eFFFFFFFA\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003cstrong style=\"color: red\"\u003e-6\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003cstrong style=\"color: red\"\u003e-0.3750\u003c/strong\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tWhoa, talk about getting a basic lesson about how computers work! PC-98 Touhou has just taught us that signedness-preserving arithmetic bitshifts are not equivalent to the apparently corresponding division by a power of two, because the typical two's complement representation of negative numbers causes the result to effectively get rounded away from zero rather than toward zero like the corresponding positive value. In our example, this means that the right lane is correct and moves at the angle we passed in, while the left lane moves \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e16\u003c/sub\u003e pixels per frame further to the left than intended. Since we're talking about the most basic piece of trigonometry code here, this inaccuracy also applies to \u003ci\u003eevery other entity in PC-98 Touhou\u003c/i\u003e that moves left relative to its origin point – and/or up, because Y coordinates are calculated analogously. Imagine that… it's been 10 years since I decompiled the first variant of this function, and I'm only now noticing how fundamentally broken it is.\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIt's understandable why master.lib's manual recommends bitshifts instead of the more correct division here. On a 486, a single 32-bit \u003ccode\u003eIDIV\u003c/code\u003e takes a whopping \u003e33 cycles, and it would have been even slower on the 286 systems that master.lib is geared toward. But there's no need to go that far: By simply rounding up negative numbers, we can emulate the rounding behavior of regular division while still using a bitshift:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003eint16_t polar_x(int16_t center, int16_t radius, uint8_t angle)\n{\n\tint32_t ret = (static_cast\u0026lt;int32_t\u0026gt;(radius) * CosTable8[angle]);\u003cspan class=\"gi\"\u003e\n+\tif(ret \u0026lt; 0) {\n+\t\t// Round the multiplication result so that the shift below will yield a number\n+\t\t// that's 1 closer to 0, thus rounding toward zero rather than away from zero as\n+\t\t// bitshifts with negative numbers would usually do. This ensures that we return\n+\t\t// the same absolute value after the bitshift that we would return if [ret] were\n+\t\t// positive, thus repairing certain broken symmetries in PC-98 Touhou.\n+\t\tret += 255;\n+\t}\u003c/span\u003e\n\treturn ((ret \u003e\u003e 8) + center);\n}\u003c/pre\u003e\u003cfigcaption\u003e\n\t\u003ca href=\"https://godbolt.org/z/dcvbcoT9z\"\u003eYou could also do this in a branchless way\u003c/a\u003e, which is coincidentally very close to what current Clang would generate if you just wrote a regular division by 256. This branchless way does seem slightly slower on a 486 though, as it adds a constant \u003e8 cycles worth of instructions. The branching implementation only adds \u003e4 cycles for positive numbers and \u003e3 for negative ones.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tBut that would be deep quirk-fixing territory. uth05win just uses floating-point math for this transformation, exchanging master.lib's 8-bit lookup tables for the C library's regular \u003ccode\u003esin()\u003c/code\u003e and \u003ccode\u003ecos()\u003c/code\u003e functions, but bypassing the issue like this also forms the single biggest source of porting inaccuracy. Can't really win here… 🤷\u003cbr\u003e\n\tNow it will be interesting to see whether ZUN worked around this inaccuracy in certain places by using slightly lower left- or up-pointing angles…\n\u003c/p\u003e\u003chr id=\"myth-2025-02-24\"\u003e\u003cp\u003e\n\tAlright, but aren't we still missing the single biggest quirk about bullets in TH02? What's with Reimu's hitbox misaligning when dying? I can't release a blog post about TH02's bullet system without solving the single most infamous bullet-related mystery that this game has to offer. So, time to start a third push for looking at all the player movement, rendering, and death sequence code…\n\u003c/p\u003e\u003cp\u003e\n\tIf you remember the code above, there is no way that a hitbox defined using hardcoded numbers can ever shift in response to anything. Any so-called hitbox misalignment would therefore be a \u003ci\u003eplayer position\u003c/i\u003e misalignment, which sounds even harder to believe. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e And sure enough, after decompiling all of it, there's nothing of that sort to be found in the player code either.\u003cbr\u003e\n\tIf we take \u003cq\u003eplayer position misalignment\u003c/q\u003e literally, we're only left with one other place where it could possibly somehow come from: the strange vertical shaking you can observe right in the first few frames of most stages. So let's visualize the hitbox and… nope, the shaking is purely a scrolling bug, nothing about it changes the internal player position used for collision detection.\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Player-shaking.webp?379f71e5\" preload=\"none\" controls data-title=\"Original game\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"64\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Player-shaking.avi?cb377283\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Player-shaking.webm?a51a0b2d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Player-shaking.webm?b08fbb9c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Player-shaking.webm?1ce06f7b\" type=\"video/webm\"\u003eVideo demonstrating TH02's shaking bug near the beginning of most stages, recorded here during the Extra Stage. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Player-shaking.avi?cb377283\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Player-shaking-overlay.webp?32afb0de\" preload=\"none\" controls data-title=\"Pellet hitbox overlay\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"64\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Player-shaking-overlay.avi?596f42fa\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Player-shaking-overlay.webm?72f4e5a6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Player-shaking-overlay.webm?bccb2aae\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Player-shaking-overlay.webm?bc665c38\" type=\"video/webm\"\u003eVideo demonstrating TH02's shaking bug near the beginning of most stages, recorded here during the Extra Stage, and with the pellet hitbox overlaid to demonstrate that this is a rendering bug and nothing that impacts logical hitboxes. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Player-shaking-overlay.avi?596f42fa\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tSo, uh, what are people even talking about? It doesn't help that \u003ca href=\"https://tcrf.net/index.php?title=Touhou_Fuumaroku:_The_Story_of_Eastern_Wonderland\u0026oldid=1471340\"\u003eno\u003c/a\u003e \u003ca href=\"https://web.archive.org/web/20190731055919/https://tvtropes.org/pmwiki/pmwiki.php/VideoGame/TouhouFuumarokuTheStoryOfEasternWonderland\"\u003eone\u003c/a\u003e cites any source for this claim and just presents it as a natural and seemingly self-evident fact, as if it was the most obvious and most easily verified property about the game.\u003cbr\u003e\n\tThankfully though, there have been two \u003ca href=\"https://youtu.be/nMv1kRd1NKA\"\u003erelatively\u003c/a\u003e \u003ca href=\"https://youtu.be/BTmIWAo4qSY\"\u003erecent\u003c/a\u003e videos about the issue, but both of them only showcase the supposed hitbox shifting in relation to a specific safespot at the end of the Extra Stage midboss. So is \u003ci\u003ethat\u003c/i\u003e what's been going on here? The community taking the game's behavior in just a single instance of collision detection within a single stage, and extending it to a general claim about the game as a whole? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tBut indeed, the described behavior cleanly reproduces every time. Enter the spot with 2 remaining lives and you survive, but enter with 1 remaining life and you die:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-playperf-2.webp?8ceb6901\" preload=\"none\" controls data-title=\"2 lives remaining\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"576\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-playperf-2.avi?3073fd21\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-playperf-2.webm?1e277b46\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-playperf-2.webm?5bd6350a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-playperf-2.webm?32374a83\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss without having died before reaching that point of the stage. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-playperf-2.avi?3073fd21\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-playperf-0.webp?7faff110\" preload=\"none\" controls data-title=\"1 life remaining\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"576\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-playperf-0.avi?aca38fac\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-playperf-0.webm?90240df3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-playperf-0.webm?624cfd2e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-playperf-0.webm?cf963c7f\" type=\"video/webm\"\u003eVideo demonstrating how the popular safespot during TH02's Extra Stage midboss breaks when having lost a life before reaching that point of the stage. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-playperf-0.avi?aca38fac\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhatever this is about, it's not due to a difference in hitboxes because Reimu's position demonstrably stays identical. But if we switch between these two videos, we can easily spot that it's the \u003ci\u003epatterns\u003c/i\u003e that are different! With 1 life left, the pattern moves at an ever so slightly slower speed, which apparently adds up to a life-or-death difference at that specific spot.\u003cbr\u003e\n\tAnd \u003ci\u003ethat's\u003c/i\u003e what the supposed hitbox shifting ultimately boils down to: The natural impact of rank on patterns, adjusting bullet speed with a factor of \u003ccode\u003e((playperf\u0026nbsp;+\u0026nbsp;48)\u0026nbsp;/\u0026nbsp;48)\u003c/code\u003e times \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e16\u003c/sub\u003e pixels. And nothing else.\u003cbr\u003e\n\tLet's visualize the hitbox and also track one of the bullets:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webp?e432202a\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = +2\u003c/code\u003e\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"160\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.avi?7ee51836\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?8168328b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?762db93d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?3d1eda2d\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss at a playperf of +2, with an overlay tracking one of the bullets that fails to intersect with Reimu's hitbox. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.avi?7ee51836\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"120\" data-title=\"First contact with hitbox\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"123\" data-title=\"Last contact\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webp?9eefaac4\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = 0\u003c/code\u003e\" loop width=\"384\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"160\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.avi?49dbb0a0\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?b0ee125b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?5966d53c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?dcdb3fba\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss at a playperf of +0, with an overlay tracking the bullets that eventually hits Reimu. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.avi?49dbb0a0\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"109\" data-title=\"Bullet hits\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf we look at the respective frames in the \u003ccode\u003eplayperf = +2\u003c/code\u003e case, we see that the bullet misses the hitbox by either one or two pixels on three successive frames:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 384px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Extra-midboss-spot-frame-120.png?d7c7fc34\" data-title=\"Frame 120\" class=\"active\" width=\"384\" alt=\"Frame 120 of the playperf = +2 video above, cropped to Reimu's position at the bottom-right edge of the playfield\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Extra-midboss-spot-frame-121.png?0fc5bd0b\" data-title=\"Frame 121\" width=\"384\" alt=\"Frame 121 of the playperf = +2 video above, cropped to Reimu's position at the bottom-right edge of the playfield\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Extra-midboss-spot-frame-122.png?9ae2c9dc\" data-title=\"Frame 122\" width=\"384\" alt=\"Frame 122 of the playperf = +2 video above, cropped to Reimu's position at the bottom-right edge of the playfield\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-02-24-TH02-Extra-midboss-spot-frame-123.png?b1a088fa\" data-title=\"Frame 123\" width=\"384\" alt=\"Frame 123 of the playperf = +2 video above, cropped to Reimu's position at the bottom-right edge of the playfield\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eThat's not a safespot, that's Reimu barely surviving only thanks to rounding.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo, for once, this is not a quirk, and doesn't even qualify as a \"funny ZUN code moment\" if you ask me. This is the game working exactly as designed, and it's the players who are instead making wild assumptions about safespots that only hold when the rank system plugs very specific numbers into the game's fixed-point math.\u003cbr\u003e\n\tIf anything, you could make the stronger case that this safespot should not work under any circumstance. If the game tested the whole parallelogram covered by a bullet's trajectory between two successive frames instead of just looking at a bullet's current position, it would consistently detect this collision regardless of rank. But even the later games don't go to these lengths.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 256px;\"\u003e\n\t\u003cimg src=\"/blog/static/2025-02-24-TH02-Extra-midboss-spot-parallelogram-detection.png?cd6a676e\" alt=\"Visualization of potential collision detection with parallelograms\" width=\"256\"\u003e\n\t\u003cfigcaption\u003eBy testing with parallelograms, the game would not only look at the distinct bullet positions in green, but also detect that the bullet traveled through the position highlighted in cyan, which does lie fully within the hitbox.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAmusingly, if you die twice before this pattern and reach a rank of -2, bullet speed drops enough for the \u003cq\u003esafespot\u003c/q\u003e to work again:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webp?e432202a\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = +2\u003c/code\u003e\" loop width=\"384\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"160\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.avi?7ee51836\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?8168328b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?762db93d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.webm?3d1eda2d\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss at a playperf of +2, with an overlay tracking one of the bullets that fails to intersect with Reimu's hitbox. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-2.avi?7ee51836\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"120\" data-title=\"First contact with hitbox\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"123\" data-title=\"Last contact\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webp?9eefaac4\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = 0\u003c/code\u003e\" loop width=\"384\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"160\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.avi?49dbb0a0\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?b0ee125b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?5966d53c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.webm?dcdb3fba\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss at a playperf of +0, with an overlay tracking the bullets that eventually hits Reimu. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-0.avi?49dbb0a0\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"109\" data-title=\"Bullet hits\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.webp?14bd1102\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = -2\u003c/code\u003e\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423\" data-frame-count=\"160\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.avi?cd34dd64\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.webm?c6efce24\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.webm?e62b9309\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.webm?378bacb9\" type=\"video/webm\"\u003eVideo of the popular safespot during TH02's Extra Stage midboss at a playperf of -2, with an overlay tracking one of the bullets that fails to intersect with Reimu's hitbox. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Extra-midboss-spot-overlay-playperf-minus2.avi?cd34dd64\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"125\" data-title=\"First contact with hitbox\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"128\" data-title=\"Last contact\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eIt's even the same bullet that fails to hit Reimu, although coming in 5 frames later.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf you're now sad because you liked the idea of ZUN deliberately putting hitbox-shifting code into the game, you don't have to be! You might have already noticed it in the 1-life videos above, but TH02 \u003ci\u003edoes\u003c/i\u003e have one funny but inconsequential instance of death-induced player position shifting. In the 19 frames between the end of the \u003cimg class=\"inline_sprite\" src=\"/blog/static/2025-02-24-TH02-MIKO.BFT-death-animation.gif?e858b43a\" alt=\"\"\u003e animation and Reimu respawning at the bottom of the playfield, ZUN just adds 4 pixels to Reimu's Y position. You don't really notice it because the game doesn't render Reimu's sprite during these frames, but this modified position still partakes in collision detection, causing bullets to be removed accordingly.\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2025-02-24-TH02-Falling-hitbox-overlay.webp?fad4844c\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"256\" style=\"aspect-ratio: 384 / 368\" data-audio data-lossless=\"/blog/static/video/zmbv/2025-02-24-TH02-Falling-hitbox-overlay.avi?8b596413\"\u003e\u003csource src=\"/blog/static/video/av1/2025-02-24-TH02-Falling-hitbox-overlay.webm?3bfe2447\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2025-02-24-TH02-Falling-hitbox-overlay.webm?e2d8d43a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2025-02-24-TH02-Falling-hitbox-overlay.webm?a07a522e\" type=\"video/webm\"\u003eVideo of TH02's falling player position in the 19 frames during the death animation where Reimu's sprite is not rendered. Also showcases the spark sprite wraparound. \u003ca href=\"/blog/static/video/zmbv/2025-02-24-TH02-Falling-hitbox-overlay.avi?8b596413\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"57\" data-title=\"Collision with second bullet\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"70\" data-title=\"Sparks spawning near top of playfield?!\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eHilariously, ZUN was well aware that this shift could move the player's Y position beyond the bottom of the playfield, and thus cause sparks to be spawned at Y coordinates larger than 400. So he just… wrapped these spark spawn coordinates back into the visible range of VRAM, thus moving them to the top of the playfield… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThe off-center spawn point of these sparks was the only actual bug in this delivery, by the way.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tTo round out the third push, I took some of the \u003ci\u003eAnything\u003c/i\u003e budget towards finalizing random bits of previously RE'd TH04 and TH05 code that wouldn't add anything more to this blog post. These posts aren't really \u003ci\u003emeant\u003c/i\u003e to be a reference – that's the job of the code, the actual primary source of the facts discussed here – but people have still started to use them as such. So it makes sense to try focusing them a bit more in the future, and not bundle all too many topics into a single one.\u003cbr\u003e\n\tThis finalization work was mostly centered on some tile rendering and .STD file loading boilerplate, but it also covered some of TH05's unfortunately undecompilable HUD number display code. The irony is that it's \u003ca href=\"https://github.com/nmlgc/ReC98/blob/165f0900e8a5bc4bc0e02b8cc655008a7946f40d/th05/main/hud/number_p.asm\"\u003eactually quite good ASM code\u003c/a\u003e that makes smart register choices and uses secondary side effects of certain instructions in a way that's clever but not overly incomprehensible. Too bad that these optimizations have no right to exist in logic code that is called way less than once per frame…\n\u003c/p\u003e\u003cp\u003e\n\tNext up: An unexpected quick return to the Shuusou Gyoku Linux port, as Arch Linux is bullying us onto SDL 3 faster than I would have liked.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-04-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2025-01-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-02-24T23:48:53Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2025-01-25",
      "url": "https://rec98.nmlgc.net/blog/2025-01-25",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-02-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-12-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2025-01-25\"\u003e\u003ctime datetime=\"2025-01-25T23:56:48Z\"\u003e2025-01-25 23:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#M0003\"\u003eM0003\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Icon commission from \u003ca href=\"https://bsky.app/profile/tremolomeasure.bsky.social\"\u003eMr. Tremolo Measure)\u003c/a\u003e\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/commit/M0003\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0299\"\u003eP0299\u003c/a\u003e\n\t\t\ttupblocks (Clang and GCC support)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/tupblocks/compare/fae347c...49255da\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0300\"\u003eP0300\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Build portability + Game logic portability, part 4/4 + SDL threads)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/M0003...dc235ca\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0301\"\u003eP0301\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Pango/Cairo text rendering, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/dc235ca...7712632\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0302\"\u003eP0302\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Pango/Cairo text rendering, part 2/2 + Linux build script + Arch Linux package)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/7712632...aec7839\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0303\"\u003eP0303\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Icon integration + Flatpak package)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/aec7839...P0303\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\tfigure.main-menu-2025-01-25 img {\n\t\tbackground-image: url('/static/sh01-main-menu-bg.png?f38e9fd7');\n\t}\n\n\tfigure#tm-2025-01-25 img {\n\t\tbackground-color: var(--c-bg);\n\t}\n\n\ttable#icons-2025-01-25 {\n\t\tfont-size: 75%;\n\t}\n\ttable#icons-2025-01-25 tr:not(:first-child) {\n\t\tborder-top: var(--table-border);\n\t\tborder-top-color: var(--c-lightgray);\n\t}\n\ttable#icons-2025-01-25 tr\u003e:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\ttable#icons-2025-01-25 tr :last-child {\n\t\ttext-align: left;\n\t}\n\ttable#icons-2025-01-25 tr :first-child {\n\t\ttext-align: right;\n\t}\n\ttable#icons-2025-01-25 .good {\n\t\tbackground-color: var(--c-trial-good);\n\t}\n\ttable#icons-2025-01-25 .mid {\n\t\tbackground-color: var(--c-trial-mid);\n\t}\n\ttable#icons-2025-01-25 .bad {\n\t\tbackground-color: var(--c-trial-bad);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tHere we go, the finale of the Shuusou Gyoku Linux port, culminating in packages for the Arch Linux AUR and Flathub! No intro, \u003ca href=\"https://bsky.app/profile/32th.bsky.social/post/3lei7ovwuvk2f\"\u003ethis is huge enough as it is\u003c/a\u003e.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#modules-2025-01-25\"\u003eCompiling with C++ Standard Library Modules for Linux\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#porting-2025-01-25\"\u003ePorting the remaining logic code to Clang\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#font-2025-01-25\"\u003ePicking a free MS Gothic replacement\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#stack-2025-01-25\"\u003eReasons for using the standard Linux text library stack\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#libs-2025-01-25\"\u003eThe individual Linux text rendering libraries\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#shifts-2025-01-25\"\u003eDebugging vertical placement issues\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#icon-2025-01-25\"\u003eThe new icon\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#pixelart-2025-01-25\"\u003eChallenges with pixel art icons in modern OS UIs\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#icon-win-2025-01-25\"\u003eWindows icon jank\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#icon-linux-2025-01-25\"\u003eLinux icon jank\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#arch-2025-01-25\"\u003ePackaging\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#arch-2025-01-25\"\u003eArch Linux\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#flatpak-2025-01-25\"\u003eFlatpak\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#future-2025-01-25\"\u003eFuture work\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#midi-2025-01-25\"\u003ePorting the MIDI backend\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#ipapatch-2025-01-25\"\u003ePatching IPAMonaGothic\u003c/a\u003e \u003c/li\u003e\u003c/ul\u003e\u003c/ol\u003e\u003chr id=\"modules-2025-01-25\"\u003e\u003cp\u003e\n\tBefore we could compile anything for Linux, I still needed to add GCC/Clang support to my Tup building blocks, in what's hopefully the last piece of build system-related work for a while. Of course, the decision to use one compiler over the other for the Linux build hinges entirely on their respective support for C++ standard library modules. I \u003ca href=\"/blog/2024-10-22#modules-2024-10-22\"\u003e📝 rolled out \u003ccode\u003eimport std;\u003c/code\u003e for the Windows build last time\u003c/a\u003e and absolutely do not want to code without it anymore. \u003ca href=\"https://en.cppreference.com/mwiki/index.php?title=Template:cpp/compiler_support/23\u0026oldid=178113#C.2B.2B23_library_features\"\u003eAccording to the cppreference compiler support table at the time I started development\u003c/a\u003e, we had the choice between\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eexperimental support in the not-yet-released GCC 15, and\u003c/li\u003e\n\t\u003cli\u003epartial support as of Clang 17, two versions ago.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\t\u003ca href=\"https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git;h=7db55c0ba1baaf0e323ef7f9ef8c9cda077d40e9\"\u003eGCC's current implementation\u003c/a\u003e does compile \u003ca href=\"https://aur.archlinux.org/packages/gcc-snapshot\"\u003ein current snapshot builds\u003c/a\u003e, but still throws lots of errors when used within the Shuusou Gyoku codebase. Clang's allegedly partial support, on the other hand, turned out just fine for our purposes. So for now, Clang it is, despite not being the preferred C/C++ compiler on most Linux distributions. In the meantime, please forgive the additional run-time dependency on \u003ccode\u003elibc++\u003c/code\u003e, its C++ standard library implementation. 🙇 Let's hope that it all \u003ci\u003ewill\u003c/i\u003e actually work in GCC 15 once that version comes out sometime in 2025.\n\u003c/p\u003e\u003cp\u003e\n\tAt a high level, my Tup building blocks only have to do a single thing to support standard library modules with a given compiler: Finding the \u003ccode\u003estd\u003c/code\u003e and \u003ccode\u003estd.compat\u003c/code\u003e module interface units at the compiler's standard locations, and compiling them with the same compiler flags used for the rest of the project. Visual Studio got the right idea about this: If you compile on its command prompts, you're already using a custom shell with environment variables that define the necessary paths and parameters for your target platform. Therefore, it makes sense to store these module units at such an easily reachable path – and sure enough, you can reliably find the \u003ccode\u003estd\u003c/code\u003e module unit at \u003ccode\u003e%VCToolsInstallDir%\\modules\\std.ixx\u003c/code\u003e. While this is hands down the optimal way of locating this file, I can understand why GCC and Clang would want module lookup to work in generic shells without polluting environment variables. In this case, asking some compiler binary for that path is a decent second-best option.\u003cbr\u003e\n\tUnfortunately, that would have been way too simple. Instead, these two compilers approached the problem from the angle of general module usage within the common build systems out there:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eUsing modules within a project introduces a new kind of dependency relation between C++ source files, forcing all such code to be compiled in an implicitly defined order. For Tup, this isn't much of a problem because it has always required \u003ca href=\"/blog/2024-07-09#tupfile-2024-07-09\"\u003e📝 order-relevant dependencies to be explicitly specified\u003c/a\u003e. So it's been quite amusing for me to hear all these CMake-entrenched CppCon speakers in recent years comment on how this aspect of modules places such a burden on build systems… 🤭\u003c/li\u003e\n\t\u003cli\u003eThen again, their goal is a world where devs just write \u003ccode\u003eimport name_of_module;\u003c/code\u003e and the build system figures out a project's dependency graph on its own by \u003ca href=\"https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.7.html\"\u003escanning all source files prior to compilation\u003c/a\u003e. Or rather, asking the compiler to parse the source files and dump out this information, using the \u003ccode\u003efdeps-*\u003c/code\u003e options on GCC, the separate \u003ccode\u003eclang-scan-deps\u003c/code\u003e tool for Clang, or the \u003ccode\u003ecl /scanDependencies\u003c/code\u003e option for MSVC.\u003c/li\u003e\n\t\u003cli\u003eBecause each of the three major compilers has its own implementation of modules, it's understandable why the options and tools are different. Obviously though, CMake is interested in at least getting all three to output the dependency information in the same format. So they got onto the C++ committee's SG15 working group and \u003ca href=\"http://wg21.link/P1689R5\"\u003eproposed a JSON format\u003c/a\u003e, which GCC and Clang subsequently implemented.\u003c/li\u003e\n\t\u003cli\u003eBut wait! The source files for the \u003ccode\u003estd\u003c/code\u003e and \u003ccode\u003estd.compat\u003c/code\u003e modules don't lie inside the source tree and couldn't be found by such a scan over the declared project files. So SG15 later \u003ca href=\"https://wg21.link/P3286R0\"\u003esimply proposed using the same JSON format for this purpose\u003c/a\u003e and installing such a JSON file together with the standard library implementation.\u003c/li\u003e\n\t\u003cli\u003eBut wait! That only shifted the problem, because now we need to find that JSON file. What does the paper have to say on that issue?\u003cblockquote style=\"white-space: unset;\"\u003e\u003cul\u003e\n\t\t\u003cli\u003eFor the Standard Library:\u003cul\u003e\n\t\t\t\u003cli\u003eThe build system should be able to query the toolchain (either the compiler or relevant packaging tools) for the location of that metadata file.\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/blockquote\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWonderful. Just what we wanted to do all along, only with an additional layer of indirection that now forces every build system to include a JSON parser somewhere in its architecture. 🤦\u003cbr\u003e\n\tIn CMake's defense, \u003ca href=\"https://www.reddit.com/r/cpp/comments/17a4t6l/comment/k5cfxw9/\"\u003ethey did try to get other build systems, including Tup, involved in these proposals\u003c/a\u003e. Can't really complain now if that was the consensus of everybody who wanted to engage in this discussion at the time. Still, what a sad irony that they \u003ca href=\"https://groups.google.com/g/tup-users/c/FhJTA6KAzWU\"\u003ereached out to Tup users\u003c/a\u003e on the exact day in 2019 at which I retired from thcrap and shelved all my plans of using Tup for modern C++ code…\n\u003c/p\u003e\u003cp\u003e\n\tSo, to locate the interface units of standard library modules on Clang and GCC, a build system must do the following:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tAsk the compiler for the path to the \u003ccode\u003emodules.json\u003c/code\u003e file, using the \u003ca href=\"https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=6a9e290eecf0f54caf6e13374428db318cb6f0cd\"\u003e30-year-old\u003c/a\u003e \u003ccode\u003e-print-file-name\u003c/code\u003e option.\u003cbr\u003e\n\tGCC and Clang implement this option in the worst possible way by basically conditionally prepending a path to the argument and then printing it back out again. If the compiler can't find the given file within its inscrutable list of paths or you made a typo, you can only detect this by string-comparing its output with your parameter. I can't imagine any use case that wouldn't prefer an error instead.\u003cbr\u003e\n\tClang was supposed to offer the conceptually saner \u003ca href=\"https://github.com/llvm/llvm-project/issues/97025\"\u003e\u003ccode\u003e-print-library-module-manifest-path\u003c/code\u003e\u003c/a\u003e option, but of course, this is modern C++, and every single good idea must be accompanied by at least one other half-baked design or implementation decision.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eLoad the JSON file with the returned file name.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eParse the JSON file.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eScan the \u003ccode\u003e\"modules\"\u003c/code\u003e array for an entry whose \u003ccode\u003e\"logical-name\"\u003c/code\u003e matches the name of the standard module you're looking for.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eDiscover that the \u003ccode\u003e\"source-path\"\u003c/code\u003e is actually relative and will need to be turned into an absolute one for your compilation command line. Thankfully, it's just relative to the path of the JSON file we just parsed.\n\u003c/p\u003e\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tSure, you can turn everything into a one-liner on Linux shells, but at what cost?\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003eclang++ -stdlib=libc++ -c -Wno-reserved-module-identifier -std=c++2c --precompile $(dirname $(clang -print-file-name=libc++.modules.json))/$(jq -r '.[\"modules\"][] | select(.\"logical-name\"==\"std\").\"source-path\"' $(clang -print-file-name=libc++.modules.json))\u003c/pre\u003e\n\t\u003cfigcaption\u003e\n\t\tYou might argue that Tup rules are a rather contrived case. Tup by itself can't store the output of processes in variables because rule generation and rule execution are two separate phases, so we need to call \u003ccode\u003eclang -print-file-name\u003c/code\u003e at both of the places in the command line where we need the file name. But, uh, \u003ca href=\"https://gitlab.kitware.com/cmake/cmake/-/blob/3c4b9cd979b9b870b151a67713f0e91f08c04b49/Modules/Compiler/Clang-CXX-CXXImportStd.cmake\"\u003eCMake's implementation is 170 lines long\u003c/a\u003e…\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAt least it's pretty straightforward to then \u003ci\u003euse\u003c/i\u003e these compiled modules. As far as our Tup building blocks are concerned, it's just another explicit input and a set of command-line flags, indistinguishable from a library. For Clang, the \u003ccode\u003e-fmodule-file=\u003ci\u003emodule_name\u003c/i\u003e=\u003ci\u003epath\u003c/i\u003e\u003c/code\u003e option is all that's required for mapping the logical module names to the respective compiled debug or release version.\u003cbr\u003e\n\tGCC, however, decided to tragically over-engineer this mapping by \u003ca href=\"https://wg21.link/p1184\"\u003edevising a \u003cspan class=\"hovertext\" title=\"Hey, at least it isn't JSON! Even though GCC does have a custom-written JSON parser in its codebase…\"\u003eplaintext\u003c/span\u003e protocol for a microservice\u003c/a\u003e like it's 2014. \u003ca href=\"https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Module-Mapper.html\"\u003eReading the usage documentation is truly soul-crushing\u003c/a\u003e as GCC tries everything in its power to not be like Clang and \u003ci\u003ejust have simple parameters\u003c/i\u003e. Fortunately, this mapper does support files as the closest alternative to parameters, which we can just \u003ccode\u003eecho\u003c/code\u003e from Tup for some \u003ca href=\"/blog/2024-07-09#prev-2024-07-09\"\u003e📝 90's response file\u003c/a\u003e nostalgia. At least I won't have to entertain this folly for a moment longer after the Lua code is written and working…\n\u003c/p\u003e\u003chr id=\"porting-2025-01-25\"\u003e\u003cp\u003e\n\tSo modules are justifiably hard and we should cut compiler writers some slack for having to come up with \u003ca href=\"https://www.reddit.com/r/cpp/comments/1b0zem7/comment/ksc91po/\"\u003ean entirely new way of serializing C++ code that still works with headers\u003c/a\u003e. But surely, there won't be any problems with the smaller new C++ features I've started using. If they've been working in MSVC, they surely do in Clang as well, right? Right…?\u003cbr\u003e\n\tOnce again, C++ standard versions are proven to be utterly meaningless to anyone outside the committee and the CppCon presenters who try to convince you they matter. Here's the list of features that still don't work in Clang in early 2025:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eC++20's \u003ca href=\"https://en.cppreference.com/w/cpp/thread/jthread\"\u003e\u003ccode\u003estd::jthread\u003c/code\u003e\u003c/a\u003e, which fixes an important design flaw of C++'s regular thread class. This would have been very unfortunate if I hadn't coincidentally already rewritten my threading code to use SDL's more portable thread API as part of the Windows 98 backport. Thus, I could adopt that work into this delivery, gifting a much-needed extra 0.3 pushes of content to the Windows 98 backport. 🙌\u003c/li\u003e\n\t\u003cli\u003eC++17's \u003ca\u003e\u003ccode\u003estd::from_chars()\u003c/code\u003e\u003c/a\u003e for floating-point values, which we use to parse \u003ca href=\"/blog/2024-03-09#peaks-2024-03-09\"\u003e📝 gain factors for waveform BGM out of Vorbis comment tags\u003c/a\u003e. This one is a medium-sized tragedy: Since it's not worth it to polyfill this function with a third-party library for just a single call, the best thing we can do is to fall back on \u003ccode\u003estrtof()\u003c/code\u003e from the C standard library. Why wasn't I using this function all along, you may ask? Well, as we all know by now, the C standard library is complete and utter trash, and \u003ccode\u003estrtof()\u003c/code\u003e is no exception by suffering from \u003ca href=\"https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f027338b0fab0f5078971fbe\"\u003elocale braindeath\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eA good \u003ccode\u003echunk()\u003c/code\u003e (ha) of the C++23 \u003ca href=\"https://en.cppreference.com/w/cpp/ranges#Range_adaptors\"\u003erange adaptors\u003c/a\u003e. As a rather new addition to the language, I've only made sporadic use of them so far to get a feel for their optimal usage. But as it turns out, sporadic use of range adaptors makes very little sense \u003ca href=\"https://github.com/nmlgc/ssg/commit/1724a999464175d4e6692d5f2c25994c16e03144\"\u003ebecause the code is much simpler and easier to read without them\u003c/a\u003e. And this is what the C++ committee has been demanding our respect for all this time? They have played us for absolute fools.\u003cbr\u003e\n\tThe \u003ccode\u003e-2\u003c/code\u003e might look slightly cryptic at first, but since this code is part of a \u003ccode\u003econstinit\u003c/code\u003e block, we'd get a compiler error if we either wrote too few elements (and left parts of the array uninitialized) or wrote too many (and thus out of the array's bounds). Therefore, the number can't be anything else.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIt almost looked like it'd finally be time for my long-drafted rant about the state of modern C++, but the language just barely redeemed itself with the last two sentences there. Some other time, then…\u003cbr\u003e\n\tOn the bright side, all my portability work on game logic code had exactly the effect I was hoping for: Everything just worked after the first successful compilation, with zero weird run-time bugs resulting from the move from a 32-bit MSVC build to 64-bit Clang. 🎉\n\u003c/p\u003e\u003chr id=\"font-2025-01-25\"\u003e\u003cp\u003e\n\tBefore we can tackle text rendering as the last subsystem that still needs to be ported away from Windows, we need to take a quick look at the font situation. Even if we don't care about pixel-perfectly matching the game's text rendering on Windows, MS Gothic seems to be the only font that fits the game's design at all:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAll text areas are dimensioned around the exact metrics of MS Gothic's embedded bitmaps. In menus, each half-width character is expected to be exactly 7×14 pixels large because most of the submenu items are aligned with spaces. In text boxes and the Music Room, glyphs can be smaller than the intended 8×16 pixels per half-width character, but they can't be larger without cutting off something somewhere.\u003c/li\u003e\n\t\u003cli\u003eOnly bitmap fonts can deliver the sharp and pixelated look the game goes for. Subpixel rendering techniques are crucial for making vector fonts look good, but quickly get ugly when applied to drop-shadowed text rendered at these small sizes:\n\t\u003cfigure class=\"fullres bglayer main-menu-2025-01-25\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Game-Start-MS-Gothic-Windows.png?03f0dc7f\"\n\t\t\tdata-title=\"Windows\"\n\t\t\twidth=\"640\"\n\t\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Game Start option selected. Rendered on Windows with MS Gothic, demonstrating that font's bitmaps for 14- and 16-pixel text heights\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Game-Start-MS-Gothic-Wine.png?7fe29402\"\n\t\t\tdata-title=\"Wine\"\n\t\t\twidth=\"640\"\n\t\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Game Start option selected. Rendered on Wine with MS Gothic, demonstrating Wine's lack of support for TTF bitmaps\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tThat's MS Gothic in both pictures. The smoothed rendering on the help text might arguably look nicer, but it clashes very badly with the drop shadow in the menus.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tHowever, MS Gothic is non-free and any use of the font outside of a Windows system violates Microsoft's EULA. In spite of that, the AUR offers three ways of installing this font regardless:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eThe \u003ca href=\"https://aur.archlinux.org/packages?O=0\u0026SeB=nd\u0026K=ttf+win+auto\u0026outdated=\u0026SB=p\u0026SO=d\u0026PP=50\u0026submit=Go\"\u003e\u003ccode\u003ettf-ms-*auto-*\u003c/code\u003e packages\u003c/a\u003e download a Windows 10 or 11 ISO from a somewhat official download link on Microsoft's CDN and extract the font files from there. Probably good enough if downloading 5\u0026nbsp;GB only to scrape a single 9\u0026nbsp;MB font file out of that image doesn't somehow feel wrong to you.\u003c/li\u003e\n\t\u003cli\u003eThe \u003ca href=\"https://aur.archlinux.org/packages?O=0\u0026SeB=nd\u0026K=ttf+win+cdn\u0026outdated=\u0026SB=p\u0026SO=d\u0026PP=50\u0026submit=Go\"\u003e\u003ccode\u003ettf-ms-win10-cdn-*\u003c/code\u003e packages\u003c/a\u003e download just the font files from… somewhere on IPFS. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eThe regular, non-\u003ccode\u003eauto\u003c/code\u003e or \u003ccode\u003e-cdn\u003c/code\u003e \u003ca href=\"https://aur.archlinux.org/packages/ttf-ms-win11\"\u003e\u003ccode\u003ettf-ms-win*\u003c/code\u003e packages\u003c/a\u003e leave it up to you where exactly you get the files from. While these are the \u003cq\u003eclearest\u003c/q\u003e options in how they let you manually perform the EULA infringement, this manual nature breaks automated AUR helpers. And honestly, requiring you to copy over all 141 font files shipped with modern Windows is massively overkill when we only need a single one of them. At that point, you might as well just copy \u003ccode\u003emsgothic.ttc\u003c/code\u003e to \u003ccode\u003e~/.local/share/fonts\u003c/code\u003e and don't bother with any package. Which, by the way, works for every distro as well as Flatpaks, which can freely access fonts on the host system.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tYou might \u003ci\u003ewant\u003c/i\u003e to go the extra mile and use any of these methods for perfectly accurate text rendering on Linux, and supporting MS Gothic should definitely be part of the intended scope of this port. But we can't expect this from everyone, and we need to find \u003ci\u003esomething\u003c/i\u003e that we can bundle as part of the Flatpak.\n\u003c/p\u003e\u003cp\u003e\n\tSo, we need an alternative free Japanese font that fits the metric constraints of MS Gothic, has embedded bitmaps at the exact sizes we need, and ideally looks somewhat close. Checking all these boxes is not too easy; Japanese fonts with a full set of all Kanji in Shift-JIS are a niche to begin with, and nobody within this niche advertises embedded bitmaps. As the DPI resolutions of all our screens only get higher, well-designed modern fonts are increasingly unlikely to have them, thus further limiting the pool to old fonts that have long been abandoned and probably only survived on websites that barely function anymore.\u003cbr\u003e\n\tUltimately, the ideal alternative turned out to be a font named IPAMonaGothic, which I found while digging through \u003ca href=\"https://github.com/Winetricks/winetricks/blob/b7cf27d70af3dc5ddbd6cd604206d1f932a7f702/src/winetricks#L14182-L14207\"\u003ethe Winetricks source code\u003c/a\u003e. While its embedded bitmaps only cover MS Gothic's first half for font heights between 10 and 16 pixels rather than going all the way to 22 pixels, it happens to be exactly the range we need for this game.\n\u003c/p\u003e\u003cfigure class=\"fullres bglayer main-menu-2025-01-25\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Game-Start-MS-Gothic-Windows.png?03f0dc7f\"\n\t\tdata-title=\"MS Gothic\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Game Start option selected. Rendered on Windows with the default MS Gothic font\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Game-Start-IPAMonaGothic-Windows.png?cc427501\"\n\t\tdata-title=\"IPAMonaGothic\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Game Start option selected. Rendered on Windows with IPAMonaGothic, demonstrating its matching metrics and roughly similar look\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tIf you're a PC-98 hardware fan, the difference between these two fonts is probably already reminding you of the stylistic difference between NEC's and Epson's versions of the ROM font.\u003cbr\u003e\n\t\tBoth of these screenshots were made on Windows. Obviously, the Linux port shouldn't settle for anything less than pixel-perfectly matching these reference renderings with both fonts.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"stack-2025-01-25\"\u003e\u003cp\u003e\n\tAlright then, how are we going to get these fonts onto the screen with something that isn't GDI? With all the emphasis on embedded bitmaps, you might come to the conclusion that all we want to do is to place these bitmap glyphs next to each other on a monospaced grid. Thus, all we'd need is a TTF/OTF library that gives us the bitmap for a given Unicode code point. Why should we use \u003ci\u003eany\u003c/i\u003e potentially system-specific API then?\u003cbr\u003e\n\tBut if we instead approach this from the point of view of GDI's feature set, it does seem better to match a standard Windows text rendering API with the equivalent stack of text rendering libraries that are typically used by Linux desktop environments. And indeed, there are also solid reasons why this is a better idea for now:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThere actually is a single instance where this game uses MS Gothic at a height of 24 pixels, which is too large to be covered by its embedded bitmaps and thus requires rasterization of vector outlines. Whenever the SCL parser encounters an unknown opcode, it shows this error message:\u003cfigure class=\"singleplayer_playfield\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-SCL-bug-MS-Gothic.png?bfce75f2\"\n\t\tdata-title=\"MS Gothic\"\n\t\twidth=\"384\"\n\t\talt=\"Screenshot of Shuusou Gyoku's SCL bug message window, reading 「バグ発生だにょ」, rendered using MS Gothic at 24px with Bézier curve rasterization rather than bitmaps.\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-SCL-bug-IPAMonaGothic.png?3fa080c1\"\n\t\tdata-title=\"IPAMonaGothic\"\n\t\twidth=\"384\"\n\t\talt=\"Screenshot of Shuusou Gyoku's SCL bug message window, reading 「バグ発生だにょ」, rendered using IPAMonaGothic at 24px with Bézier curve rasterization rather than bitmaps.\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tModders may very well end up seeing this one as a result of bugs in SCL compilers.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eYou might see debug text as not worth bothering with, but then there's Kioh Gyoku. Not only does that game display its text at much bigger sizes throughout, but it also renders every string at 3× the size it is ultimately downscaled to, similar to the 2× scale factor used by the 640×480 Windows Touhou games. Going for a full-featured solution that works with both embedded bitmaps and outlines saves us time later.\u003c/li\u003e\n\t\u003cli\u003eWe'd be ready for translations into even the most complex-to-render non-ASCII scripts.\u003c/li\u003e\n\t\u003cli\u003eSince our fonts might not support these scripts, having the API fall back on other fonts installed in the system as necessary would allow us to add these translations independently of figuring out the font situation for them.\u003c/li\u003e\n\t\u003cli\u003eIn fact, text rendering must technically already support glyph fallback because \u003ca href=\"/blog/2024-03-09#impl-2024-03-09\"\u003e📝 the BGM pack selection just displays path names\u003c/a\u003e, which  count as user input. If people use code points in their BGM pack folder names that aren't covered by either of our two fonts, they probably have \u003ci\u003esome\u003c/i\u003e font installed on their  system that can display them. Also, the missing .DAT file screen further below in that post shows that GDI already does glyph fallback with emoji, so wouldn't it be lame if the Linux version didn't have at least feature parity in this regard? Instead, the Linux stack would actually outperform GDI thanks to the former's natural support for color emoji. 🎨\u003c/li\u003e\n\t\u003cli\u003eSince we're explicitly porting to desktop Linux here, using the standard Linux text rendering stack is the least bloated option because Linux users will have it installed anyway. We can still reach for more minimalistic alternatives later once we do port this game to something other than Linux.\u003c/li\u003e\n\u003c/ul\u003e\u003cp id=\"libs-2025-01-25\"\u003e\n\tLet's look at what this stack consists of and how the libraries interact with each other:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003eFreeType provides access to everything related to the rendering of TTF and OTF fonts, including their embedded bitmaps, as well as a rasterizer for regular vector glyphs. It's completely obvious why we need this library.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eGLib2 is a collection of various general utility functions that modern non-C languages would have in their standard libraries. Most notably, it provides the tables and APIs for Unicode character data, but its \u003ccode\u003eiconv\u003c/code\u003e wrapper also comes in quite handy for converting the Shift-JIS text from the original .DAT files to UTF-8 without additional dependencies.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eFriBidi implements the Unicode Bidirectional Algorithm, just in case you've thrown some Arabic or Hebrew into your string.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eHarfBuzz implements shaping, i.e., the translation of raw Unicode into a sequence of glyph indices and positions depending on what's supported by the font. We might not \u003ci\u003estrictly\u003c/i\u003e need this library right now, but it's completely obvious why we will eventually need it for translations.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eFontconfig manages all fonts installed on the system, maps user-friendly font names to file names, tracks their Unicode coverage, and offers a central place for storing \u003ca href=\"https://wiki.archlinux.org/title/Font_configuration/Examples\"\u003evarious font tweaking options\u003c/a\u003e.\u003cbr\u003e\n\tNormally, games wouldn't need this library because they just bundle all the fonts they need and hardcode any required tweaking settings to make them look as intended. Looking back at our font situation though, installing MS Gothic in a system-wide way through a package that puts the font into a standard location will be the simplest method of meeting that optional dependency. This is a reasonable assumption in a neatly packaged Linux system where the font is just another item on the game's dependency list, but also within a Flatpak, where \"system-wide\" includes any fonts shipped with the image. If we now assume that IPAMonaGothic is installed in the same way, we can let Fontconfig handle the actual selection. All we need to do is to specify a preference for MS Gothic over IPAMonaGothic, and Fontconfig will take care of the rest, without us having to write a single line of TTF-loading code.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003ePango combines the three libraries above into an API that somewhat matches GDI's simplicity, laying out text in one or multiple lines based on the shaped output of HarfBuzz and substituting glyphs as necessary based on Fontconfig information. The actual rendering, however, is delegated to…\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eCairo, a… \"2D graphics library\"? Now why would we need one of those if all we want is a buffer filled with pixels? \u003ca href=\"https://en.wikipedia.org/wiki/Cairo_(graphics)\"\u003eWikipedia's description emphasizes its vector graphics capabilities\u003c/a\u003e, which \u003ci\u003eseems\u003c/i\u003e to describe the library better than the \u003ca href=\"https://www.cairographics.org/\"\u003enondescript blurb on its official website\u003c/a\u003e, but doesn't FreeType already do this for text? After looking at it for way too long, the best summary I can come up with is \"a collection of font rasterization code that should have maybe been part of FreeType, plus the aforementioned general 2D vector graphics code we don't need\". Just like Pango wraps HarfBuzz and Fontconfig to lay out the individual glyphs, Cairo wraps FreeType and raw pixel buffers to actually place these glyphs on its surface abstraction. (And also Fontconfig because of all its configuration settings that can influence the rendering.) Ultimately, this means that each font is represented by a HarfBuzz+FreeType handle, a Pango+Cairo handle, and a Cairo+FreeType handle, which I'm sure won't be relevant later on. 👀\u003cbr\u003e\n\tPango does have a raw FreeType backend that could render text entirely without Cairo, but it's not really maintained and supports neither embedded bitmaps nor color emoji. So we don't have much of a choice in the matter.\u003c/p\u003e\u003cfigure style=\"width: 100%;\"\u003e\n\t\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\t\tCreated using \u003ccode\u003epango-view -t 'effective. Power لُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ🌈冗' --font='MS Gothic 16px' --backend=cairo\u003c/code\u003e.\n\t\t\u003c/div\u003e\u003cdiv\u003e\n\t\t\tCreated using \u003ccode\u003epango-view -t 'effective. Power لُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ🌈冗' --font='MS Gothic 16px' --backend=ft2\u003c/code\u003e.\n\t\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-01-25-Pango-Cairo.png?6a7b16d6\"\n\t\t\tdata-title=\"Pango/Cairo\"\n\t\t\twidth=\"275\"\n\t\t\talt=\"The infamous iOS Effective Power crash string with an additional rainbow emoji as rendered by Pango with the pango-view tool using MS Gothic at a height of 16 pixels and Pango's Cairo backend, which renders the string as expected with MS Gothic's embedded bitmaps and a colored emoji\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2025-01-25-Pango-ft2.png?9f32e37e\"\n\t\t\tdata-title=\"Pango/FreeType\"\n\t\t\twidth=\"275\"\n\t\t\talt=\"The infamous iOS Effective Power crash string with an additional rainbow emoji as rendered by Pango with the pango-view tool using MS Gothic at a height of 16 pixels and Pango's FreeType2 backend, demonstrating a lack of support for embedded bitmaps and color emoji as well as completely broken emoji placement\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003c/figure\u003e\n\t\u003cp\u003eFun fact: Since Cairo also manages the temporary CPU image buffer we draw on and then hand to SDL, our backend for Shuusou Gyoku ends up with 3× the amount of Cairo function calls than Pango function calls.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003ePixman is the library that actually performs all the management of and operations on pixel buffers that you would have thought to be Cairo's job. The combination of it also being a core dependency of the X server and \u003ca href=\"https://www.pixman.org/\"\u003enot having any documentation\u003c/a\u003e gives off much stronger \u003ca href=\"https://www.xkcd.com/2347/\"\u003eNebraska\u003c/a\u003e vibes than \u003ca href=\"https://github.com/harfbuzz/harfbuzz/blob/3a7ebc320d0038ebf61c60531979a6f9bca7c26b/README.md\"\u003ethe ones HarfBuzz advertises itself with\u003c/a\u003e. Initially, the dependency on this library comes off as completely useless because Pango's FreeType backend doesn't need anything like it, but judging by the presence of \u003ca href=\"https://gitlab.freedesktop.org/pixman/pixman/-/tree/master/pixman?ref_type=heads\"\u003eoptimized blitting and scaling implementations for various CPU instruction set extensions\u003c/a\u003e, it seems to do a pretty good job at what it does. Unlike Cairo, whose abstraction reduces \u003ca href=\"https://gitlab.freedesktop.org/pixman/pixman/-/blob/707d7e34ca3d435b23ac3e7c05417fc0d39f4263/pixman/pixman.h#L884-896\"\u003ePixman's support for a variety of 32-bit color formats\u003c/a\u003e to \u003ca href=\"https://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t\"\u003ethe single ARGB one\u003c/a\u003e. We're very lucky that this format is also supported for textures by all SDL backends on all operating systems…\u003c/p\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tIn the end, a typical desktop Linux program requires every single one of these 8 libraries to end up with a combined API that resembles Ye Olde Win32 GDI in terms of functionality and abstraction level. Sure, the combination of these eight is more powerful than GDI, offering e.g. \u003ca href=\"https://www.cairographics.org/manual/cairo-Transformations.html\"\u003eaffine transformations\u003c/a\u003e and \u003ca href=\"https://gitlab.gnome.org/GNOME/pango/-/blob/74cebf36892dc4267ae45f88cae5dc0b16cdae38/examples/cairotwisted.c\"\u003etext rendering along a curved path\u003c/a\u003e. But you can't \u003ci\u003eremove\u003c/i\u003e any of these libraries without falling behind GDI.\n\u003c/p\u003e\u003cp\u003e\n\tEven then, my Linux implementation of text rendering for Shuusou Gyoku still ended up slightly longer than the GDI one due to all the Pango and Cairo contexts we have to manually manage. But I did come up with a nice trick to reduce at least our usage of Cairo: Since GDI needs to be used together with DirectDraw, the GDI implementation must keep a system-memory copy of the entire \u003ca href=\"/blog/2023-08-01#text-2023-08-01\"\u003e📝 text surface\u003c/a\u003e due to \u003ca href=\"/blog/2023-08-01#ddraw-2023-08-01\"\u003e📝 DirectDraw's possibility of surface loss\u003c/a\u003e. But since we only use Cairo with SDL, the Cairo surface in system memory does not actually need to match the SDL-managed GPU texture. Thus, we can reduce the Cairo surface to the role of a merely temporary system-memory buffer that only is as large as the single largest text rectangle, and then copy this single rectangle to the intended packed place within the texture. I probably wouldn't have realized this if the seemingly most simple way to limit rendering to a fixed rectangle within a Cairo surface didn't involve \u003ca href=\"https://www.cairographics.org/manual/cairo-cairo-surface-t.html#cairo-surface-create-for-rectangle\"\u003ecreating another Cairo surface\u003c/a\u003e, which turned out to be quite cumbersome.\n\u003c/p\u003e\u003chr id=\"shifts-2025-01-25\"\u003e\u003cp\u003e\n\tBut can this stack deliver the pixel-perfect rendering we'd like to have? Well, \u003ci\u003ealmost\u003c/i\u003e:\n\u003c/p\u003e\u003cfigure class=\"side_by_side main-menu-2025-01-25\"\u003e\u003cfigure class=\"fullres\"\u003e\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Extra-Start-MS-Gothic-Windows.png?daf396bc\"\n\t\tdata-title=\"MS Gothic, Windows\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Extra Start option selected, rendered on Windows with the default MS Gothic font.\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Extra-Start-MS-Gothic-Pango.png?f88d2b09\"\n\t\tdata-title=\"MS Gothic, Pango 1.56.0 / Cairo\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Extra Start option selected, rendered on Linux using Cairo and version 1.56.0 of Pango with MS Gothic, demonstrating how all 14-pixel text is shifted down by one pixel and how all Japanese characters in 16-pixel text are pushed down by 2 pixels if there are Latin characters on the same line\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cfigure class=\"fullres\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Extra-Start-IPAMonaGothic-Windows.png?8879409c\"\n\t\tdata-title=\"IPAMonaGothic, Windows\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Extra Start option selected, rendered on Windows with IPAMonaGothic.\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-SH01-Main-menu-Extra-Start-IPAMonaGothic-Pango.png?642b85a0\"\n\t\tdata-title=\"IPAMonaGothic, Pango 1.56.0 / Cairo\"\n\t\twidth=\"640\"\n\t\talt=\"Screenshot of Shuusou Gyoku's main menu with the Extra Start option selected, rendered on Linux using Cairo and version 1.56.0 of Pango with IPAMonaGothic, demonstrating how all text is shifted down by one pixel\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003c/figure\u003e\u003cp\u003e\n\tCue hours of debugging to find the cause behind these vertical shifts. The overview above already suggested it, but this bug hunt really drove home how this entire stack of libraries is a huge pile of redundantly implemented functionality that interacts with and overrides each other in undocumented and mostly unconfigurable ways. Normally, I don't have much of a problem with that as long as I can step through the code, but stepping through Cairo and especially Pango is a special kind of awful. Both libraries implement dynamic typing and object-oriented paradigms in C, thus hiding their actually interesting algorithms under layers and layers of \"clean\" management functions. But the worst part is a particularly unexpected piece of recursion: To layout a paragraph of text, Pango requires a few font metrics, which it calculates by laying out a language-specific paragraph of example text. No, I do not like stepping through functions \u003ci\u003ethat\u003c/i\u003e much, please don't put a call to the text layout function into the text layout function to make me debug while I debug, dawg…\u003cbr\u003e\n\tIt'll probably take many more years until \u003ca href=\"https://behdad.org/text2024/\"\u003emost of this stack has been displaced with the planned Rust rewrites\u003c/a\u003e. But honestly, I don't have great hopes as long as they stay with this pile-of-libraries approach. This pile doesn't even deserve to be called a stack given \u003ca href=\"https://github.com/harfbuzz/harfbuzz/issues/2524\"\u003ethe circular dependency between FreeType and HarfBuzz\u003c/a\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tUltimately, these are the bugs we're seeing here:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\n\t\u003cp\u003eWhen rendering strings that contain both Japanese and Latin characters with MS Gothic, the Japanese characters are pushed down by about \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e8\u003c/sub\u003eth of the font height. This one \u003ca href=\"https://discourse.gnome.org/t/16010\"\u003ewas already reported in June 2023\u003c/a\u003e and is \u003ca href=\"https://github.com/harfbuzz/harfbuzz/issues/4311\"\u003ea bug in either HarfBuzz, Pango, or MS Gothic\u003c/a\u003e. With the main HarfBuzz developer confused and without an idea for a clean solution, the bug has remained unfixed for 1½ years.\u003cbr\u003e\n\tFor now, the best workaround would be to \u003ca href=\"https://gitlab.gnome.org/GNOME/pango/-/commit/539ca68c7725f9d7bfb6ba8ad3c94f354a6c1142\"\u003erevert the commit that introduced the baseline shift\u003c/a\u003e. Since the Flatpak release can bundle whatever special version of whatever library it needs, I can patch this bug away there, but distro-specific packages or self-compiled builds would have to \u003ca href=\"https://github.com/flathub/net.nmlgc.rec98.sh01/blob/be7a5a249bcf72e1dc0bfa24895610b0ef5cbc8e/pango-workaround-harfbuzz-issue-4311.patch\"\u003epatch\u003c/a\u003e Pango themselves. \u003ccode\u003eLD_LIBRARY_PATH\u003c/code\u003e is a clean way of opting into the patched library without interfering with the regular updates of your distro, but there's still a definite hurdle to setting it up.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe remaining 1-pixel vertical shift is, weirdly enough, caused by \u003ca href=\"https://en.wikipedia.org/wiki/Font_hinting\"\u003ehinting\u003c/a\u003e. Now why would a technique intended for improving the sharpness of outline fonts even apply to bitmap fonts to begin with? As you might have guessed, the pile-of-libraries approach strikes once more:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\t\u003cp\u003eHinting is \u003ca href=\"https://wiki.archlinux.org/title/Font_configuration/Examples#Hinted_fonts\"\u003emeant to be controlled by Fontconfig settings\u003c/a\u003e, but the setting that takes precedence here is \u003ca href=\"https://www.cairographics.org/manual/cairo-cairo-font-options-t.html#cairo-hint-metrics-t\"\u003eCairo's slightly different \u003ci\u003emetric\u003c/i\u003e hint setting\u003c/a\u003e, which is enabled by default.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003ePango then responds to Cairo's hinting request by \u003ca href=\"https://gitlab.gnome.org/GNOME/pango/-/blob/c5899494f57b83d54712917cc00ccb4841bd8981/pango/pangocairo-fcfont.c#L84\"\u003erounding the font's ascent and descent metrics up to the nearest integer\u003c/a\u003e, causing the exact downward shift we see above.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eWe can override Cairo's metric hinting defaults with the API documented in the page I linked above. But we must only do so conditionally because 16-pixel MS Gothic \u003ci\u003edoes\u003c/i\u003e require metric hinting for its glyph placement to match GDI. \u003ca href=\"https://github.com/nmlgc/ssg/blob/c117cc186fca0804a49df51321f825d84243f2e9/platform/pangocairo/text_pangocairo.cpp#L38-L84\"\u003eThe resulting hack is very much not pretty\u003c/a\u003e.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eCairo's font options can only be really changed at the level of a Cairo context. Any Pango font handle created from a Pango layout mapped to a Cairo context will get a copy of that context's font options at creation time. And \u003ci\u003eof course\u003c/i\u003e, the Pango level treats these options as an implementation detail that cannot be modified from the outside. So, we need to figure out the font using raw Fontconfig calls instead of \u003ca href=\"https://docs.gtk.org/Pango/class.FontMap.html\"\u003ePango's abstraction\u003c/a\u003e. Oh, and this copy also forces us to recreate the Pango layout if we change between 14- and 16-pixel MS Gothic, which is not necessary with IPAMonaGothic.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eActually overwriting this setting involves creating a new font option object, filling it with the Cairo context's existing font options, modifying the setting, copying the new font option object back to the Cairo context, and then deleting the temporary font option object. This is real C, done by real C programmers. Reminds me of \u003ca href=\"https://github.com/nmlgc/ReC98/blob/71f481094259fb42b47e75267c2e89774b268a51/th01/main/boss/entity_a.hpp#L53-L84\"\u003ethe one place in TH01 where ZUN tried C++ copy constructors\u003c/a\u003e for \u003ca href=\"https://github.com/nmlgc/ReC98/commit/af25fa19910956ccf1c2a385923af9363a71a63e\"\u003ea class that didn't need them at all, which only added 1,056 bytes of bloat to \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\t\u003c/li\u003e\u003c/ul\u003e\n\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tDon't you love it when the concerns are so separated that they end up overlapping again? I'm \u003ci\u003eso\u003c/i\u003e looking forward to writing my own bitmap font renderer for the multilingual PC-98 translations, where the memory constraints of conventional DOS RAM make it infeasible to use any libraries of this pile to begin with 😛\n\u003c/p\u003e\u003chr id=\"icon-2025-01-25\"\u003e\u003cp\u003e\n\tBefore we can package this port for Flathub, there's one more obstacle we have to deal with. \u003ca href=\"https://discourse.flathub.org/t/386\"\u003eFlathub mandates that any published and publicly listed app must come with an icon that's at least 128×128 pixels in size.\u003c/a\u003e pbg did not include the game's original 32×32 icon in the MIT-licensed source code release, but even if he did, just taking that icon and upscaling it by 4× would simultaneously look lame \u003ci\u003eand\u003c/i\u003e more official than it perhaps should.\u003cbr\u003e\n\tSo, the backers decided to commission a new one, depicting VIVIT in her title screen pose but drawn in a different style as to not look too official. Mr. Tremolo Measure quickly responded to our search and Ember2528 liked his PC-98-esque pixel art style, so that's what we went for:\n\u003c/p\u003e\u003cfigure id=\"tm-2025-01-25\" class=\"pixelated\" style=\"width: 384px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-016.png?b4ffeada\"\n\t\tdata-title=\"16×16\"\n\t\twidth=\"384\"\n\t\talt=\"The 16×16 version of the new Shuusou Gyoku icon commissioned from Mr. Tremolo Measure\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-032.png?38cbcd51\"\n\t\tdata-title=\"32×32\"\n\t\twidth=\"384\"\n\t\talt=\"The 32×32 version of the new Shuusou Gyoku icon commissioned from Mr. Tremolo Measure\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-048.png?67a1addc\"\n\t\tdata-title=\"48×48\"\n\t\twidth=\"384\"\n\t\talt=\"The 48×48 version of the new Shuusou Gyoku icon commissioned from Mr. Tremolo Measure\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-128.png?96524b5d\"\n\t\tdata-title=\"128×128\"\n\t\twidth=\"384\"\n\t\talt=\"The 128×128 version of the new Shuusou Gyoku icon commissioned from Mr. Tremolo Measure\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tMr. Tremolo Measure on \u003ca href=\"https://bsky.app/profile/tremolomeasure.bsky.social\"\u003eBluesky\u003c/a\u003e.\u003cbr\u003e\n\t\t\u003ca href=\"https://github.com/nmlgc/ssg/tree/master/art\"\u003eThe repo also contains textless and boxless variants.\u003c/a\u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp id=\"pixelart-2025-01-25\"\u003e\n\tHowever, the problem with pixel art icons is that they're strongly tied to specific resolutions. This clashes with modern operating system UIs that want to almost arbitrarily scale icons depending on the context they appear in. You can still \u003ci\u003ego\u003c/i\u003e for pixel art, and it sure looks gorgeous if their resolution exactly matches the size a GUI wants to display them at. But that's a big \u003ci\u003eif\u003c/i\u003e – if the size \u003ci\u003edoesn't\u003c/i\u003e match and the icon gets scaled, the resulting blurry mess lacks all the definition you typically expect from pixel art. Even nearest-neighbor integer upscaling looks more cheap rather than stylized as the coarse pixel grid of the icon clashes with the finer pixel grid of everything surrounding it.\n\u003c/p\u003e\u003cp id=\"icon-win-2025-01-25\"\u003e\n\tSo you'd want multiple versions of your icon that cover all the exact sizes it will appear at, which is definitely more expensive than a single smooth piece of scalable vector artwork. On a cursory look through Windows 11, I found no fewer than 7 different sizes that icons are displayed at:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e16×16 in the title bar and all of Explorer's list views\u003c/li\u003e\n\t\u003cli\u003e24×24 in the taskbar\u003c/li\u003e\n\t\u003cli\u003e28×28 in the small icon next to the file name in Explorer's detail pane (which is never sharp for some reason, even if you provide a 28×28 variant?!)\u003c/li\u003e\n\t\u003cli\u003e32×32 in the old-style \u003ci\u003eProperties\u003c/i\u003e window\u003c/li\u003e\n\t\u003cli\u003e48×48 in Explorer's \u003ci\u003eMedium icons\u003c/i\u003e view\u003c/li\u003e\n\t\u003cli\u003e96×96 in Explorer's \u003ci\u003eLarge icons\u003c/i\u003e view, and the large icon its detail pane\u003c/li\u003e\n\t\u003cli\u003e256×256 in Explorer's \u003ci\u003eExtra large icons\u003c/i\u003e view\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd that's just at 1× display scaling and the default zooming factors in Explorer.\n\u003c/p\u003e\u003cp\u003e\n\tBut it gets worse. Adding our commissioned multi-resolution icon to an .exe seems simple enough:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eBundle the individual images into a single .ico file using \u003ccode\u003emagick in1.png in2.png … out.ico\u003c/code\u003e\u003c/li\u003e\n\t\u003cli\u003eWrite \u003ca href=\"https://github.com/nmlgc/ssg/blob/9e255e822b9cdc2ace2039f698f0b631cc72600c/GIAN07/GIAN07.rc\"\u003ea small resource script\u003c/a\u003e, call \u003ccode\u003erc\u003c/code\u003e, and add the resulting .res file to the link command line\u003c/li\u003e\n\t\u003cli\u003eBe amazed as that icon appears in the title and task bars without you writing a single line of code, thanks to SDL's window creation code automatically setting the first icon it finds inside the executable\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tBut what's going on in Explorer?\n\u003c/p\u003e\u003cfigure style=\"width: 271px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-Explorer-ICO.png?624e1125\"\n\t\tdata-title=\"\u003ccode\u003e.ico\u003c/code\u003e\"\n\t\twidth=\"271\"\n\t\talt=\"An .ico file of the new Shuusou Gyoku icon commissioned from Mr. Tremolo Measure. Explorer's extra large icon mode shows the highest-resolution 128×128-pixel variant in a 128×128-pixel box, as expected.\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-TremoloMeasure-Explorer-EXE.png?5951fc6f\"\n\t\tdata-title=\"\u003ccode\u003e.exe\u003c/code\u003e\"\n\t\twidth=\"271\"\n\t\talt=\"An .exe binary with the same .ico file embedded. Strangely, Explorer's extra large icon mode shows the 48×48-pixel variant in the center of a 256×256-pixel box.\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tSame \u003ci\u003eExtra large icons\u003c/i\u003e setting for both.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's the 48×48 variant sitting all tiny in the center of a 256×256 box, in a context where we expect exactly what we get for the .ico file. Did I just stumble right into the next underdocumented detail? What was the point of having a different set of rules for icons in .exe files? Make that \u003ca href=\"/blog/2023-09-30#fps-2023-09-30\"\u003e📝 another\u003c/a\u003e Raymond Chen explanation I'm dying to hear…\u003cbr\u003e\n\tUntil then, here's what the rules appear to be:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e256×256 is the one and only mandatory size for high-res program icons on Windows.\u003c/li\u003e\n\t\u003cli\u003e48×48 is the next smallest supported size, as unbelievable as that sounds. Windows will never use any other icon variant in between. \u003ca href=\"https://www.axialis.com/tutorials/tutorial-vistaicons.html\"\u003eSome sites claim that 64×64 is supported as well\u003c/a\u003e, but I sure couldn't confirm that in my tests.\u003c/li\u003e\n\t\u003cli\u003eThose 96×96 use cases from the list above? Yup, Windows will never actually display an embedded 96×96 icon at its native resolution, and either scale up the 48×48 variant (in the \u003ci\u003eLarge icons\u003c/i\u003e view) or scale down the 256×256 variant (in the detail pane).\u003c/li\u003e\n\t\u003cli\u003eYou only ever see an embedded icon with a size between 48×48 and 256×256 if it's the only icon available – and then it still gets scaled to 48×48. Or to 96×96, depending on how Explorer feels like.\u003c/li\u003e\n\t\u003cli\u003eGetting different results in your tests? Try \u003ca href=\"https://www.elevenforum.com/t/rebuild-icon-cache-in-windows-11.2049/\"\u003erebuilding the icon cache\u003c/a\u003e, because \u003ci\u003eof course\u003c/i\u003e Windows still struggles with cache invalidation. This must have caused unspeakable amounts of miscommunication with artists over the decades.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOh well, let's nearest-neighbor-scale our 128×128 icon by 2× and move on to Linux, where we won't have such archaic restrictions…\n\u003c/p\u003e\u003cp id=\"icon-linux-2025-01-25\"\u003e\n\t…which is not to say that pixel art icons \u003ci\u003edon't\u003c/i\u003e come with their own issues there. 🥲\u003cbr\u003e\n\tOn Linux, this kind of metadata is not part of the ELF format, but is typically stored in separate \u003ca href=\"https://specifications.freedesktop.org/desktop-entry-spec/latest/\"\u003eDesktop Entry files\u003c/a\u003e, which are analogous to .lnk shortcuts on Windows. Their plaintext nature already suggests that icon assignment is refreshingly sane compared to the craziness we've seen above, and indeed, you simply refer to PNG or even SVG files in a \u003ca href=\"https://specifications.freedesktop.org/icon-theme-spec/latest/\"\u003eseparate directory tree that supports arbitrary size variants and even different themes\u003c/a\u003e. For non-SVG icons, menus and panels can then pick the best size variant depending on how many pixels they allot to an icon. The overwhelming majority of the ones I've seen do a good job at picking exactly the icon you'd expect, and bugs are rare.\n\u003c/p\u003e\u003cp\u003e\n\tBut how would this work for title and task bars once you started the app? If you launched it through a Desktop Entry, a smart window manager might remember that you did and automatically use the entry's icon for every window spawned by the app's process. Apparently though, this feature is rather rare, maybe because it only covers this single use case. What about just directly starting an app's binary from a shell-like environment without going through a Desktop Entry? You wouldn't expect window managers to \u003cspan class=\"hovertext\" title=\"Also, this mapping isn't guaranteed to be unique.\"\u003emaintain a reverse mapping from binaries to Desktop Entries\u003c/span\u003e just to also support icons in this other case.\n\u003c/p\u003e\u003cp\u003e\n\tSo, there must be some way for a program to tell the window manager which icon it's supposed to use. Let's see what SDL has to offer… and the documentation only lists \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_SetWindowIcon\"\u003ea single function that takes a single image buffer\u003c/a\u003e and transfers its pixels to the X11 or Wayland server, overriding any previous icon. 😶\u003cbr\u003e\n\tWell great, another piece of modern technology that works against pixel art icons. How can we know which size variant we should pick if icon sizing is the job of the window manager? For the same reason, this function used to be unimplemented in the Wayland backend \u003ca href=\"https://github.com/libsdl-org/SDL/pull/11111/commits/75ab5eb8d9d6cd3fce312d7ab9244acd4fe8639e\"\u003euntil the committee of Wayland stakeholders agreed on the \u003ccode\u003exdg-toplevel-icon\u003c/code\u003e protocol last year\u003c/a\u003e.\u003cbr\u003e\n\tNow, we could \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_GetWindowBordersSize\"\u003equery the size of the window decorations at all four edges\u003c/a\u003e to at least get an approximation, but that approach creates even more problems:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhich edge do we pick? The top one? The largest one? How can we possibly be sure that the one we pick is the one that will show the icon?\u003c/li\u003e\n\t\u003cli\u003eEven if we picked the correct edge, the icon will likely be smaller and not cover the full area. Again, anything less than an exact match isn't good enough for pixel art.\u003c/li\u003e\n\t\u003cli\u003eThis function is not implemented on Wayland because client windows aren't supposed to care about how the server is decorating them.\u003c/li\u003e\n\t\u003cli\u003eBut even among X11 window managers, there's at least one that doesn't report back the border sizes immediately after window creation. 🙄\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tMost importantly though: What if that icon is also used in a taskbar whose icons have a different size than the ones in title bars? Both X11's \u003ccode\u003e_NET_WM_ICON\u003c/code\u003e property and Wayland's \u003ccode\u003exdg-toplevel-icon-v1\u003c/code\u003e protocol support multiple size variants, but SDL's function does not expose this possibility. It might look as if SDL 3 supports this use case via its \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_AddSurfaceAlternateImage\"\u003enew support for alternate images in surfaces\u003c/a\u003e, but this feature is currently only used for mouse cursors. That sounds like a pull request waiting to happen though, I can't think of a reason not to do the same for icons. \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/contribution-ideas\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/a\u003e\u003c/span\u003e?\n\u003c/p\u003e\u003cp\u003e\n\tBut if SDL 2's single window icon function used to be unsupported on Wayland, did SDL 2 apps just not have icons on Wayland before October 2024?\u003cbr\u003e\n\tDigging deeper reveals the tragically undocumented \u003ccode\u003eSDL_VIDEO_X11_WMCLASS\u003c/code\u003e environment variable, which does what we were hoping to find all along. If you set it to the name of your program's Desktop Entry file, the window manager is supposed to locate the file, parse it, read out the \u003ca href=\"https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html#key-icon\"\u003e\u003ccode\u003eIcon\u003c/code\u003e value\u003c/a\u003e, and perform the usual icon and size lookup. Window class names are a standard property in both X11 and Wayland, and since \u003ca href=\"https://github.com/libsdl-org/SDL/blob/3f02118264e0b2d028e6eb9a8aad67725a7c335e/src/video/wayland/SDL_waylandvideo.c#L98-L108\"\u003eSDL helpfully falls back on this variable even on Wayland\u003c/a\u003e, it will work on both of them.\n\u003c/p\u003e\u003cp\u003e\n\tOr at least it \u003ci\u003eshould\u003c/i\u003e. Ultimately, it's up to the window manager to actually implement class-derived icons, and sadly, correct support is not as widespread as you would expect.\u003cbr\u003e\n\tHow would I know this? Because I've tested them all. 🥲 That is, all non-AUR options listed on the Arch Wiki's \u003ccite\u003e\u003ca href=\"https://wiki.archlinux.org/index.php?title=Desktop_environment\u0026oldid=820122\"\u003eDesktop environment\u003c/a\u003e\u003c/cite\u003e and \u003ccite\u003e\u003ca href=\"https://wiki.archlinux.org/index.php?title=Window_manager\u0026oldid=823802\"\u003eWindow manager\u003c/a\u003e\u003c/cite\u003e pages that provide something vaguely resembling a desktop you can launch arbitrary programs from:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ctable id=\"icons-2025-01-25\"\u003e\n\t\t\u003cthead\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003eWM / DE\u003c/th\u003e\n\t\t\t\u003cth\u003eManually transferred pixels\u003c/th\u003e\n\t\t\t\u003cth\u003eClass-derived icons\u003c/th\u003e\n\t\t\t\u003cth\u003eNotes\u003c/th\u003e\n\t\t\u003c/tr\u003e\u003c/thead\u003e\n\t\t\u003ctbody\u003e\n\t\t\t\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eawesome\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eDoes not report border sizes back to SDL immediately after window creation\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eBlackbox\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003ebspwm\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eNo title bars\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003eBudgie\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003eTitle bars have no icons. Taskbar falls back on the icon from the Desktop Entry file the app was launched with.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003eCinnamon\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003eTitle bars have no icons, but they work fine in the taskbar. Points out the difference between native and Flatpak apps!\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003eCOSMIC\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003eTitle bars have no icons, but they work fine in the taskbar. Points out the difference between native and Flatpak apps!\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eCutefish\u003c/th\u003e\u003ctd\u003e➖\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eTitle bars have no icons. The status bar only seems to support the X11 \u003ccode\u003e_NET_WM_ICON\u003c/code\u003e property, and not the older \u003ccode\u003eXWMHints\u003c/code\u003e mechanism used by e.g. xterm.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eDeepin\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eDid not start\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eEnlightenment\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e➖\u003c/td\u003e\u003ctd\u003eTaskbar falls back on the icon from the Desktop Entry file the app was launched with. Only picks the correctly scaled icon variant in about half of the places, and just scales the largest one in the other half.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eFluxbox\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eGNOME Flashback / Metacity\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eTitle bars have no icons\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003eGNOME\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003eTitle bars have no icons\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eGNOME Classic\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eHow do you get this running? \u003ca href=\"https://wiki.archlinux.org/index.php?title=GNOME\u0026oldid=824605#Manually\"\u003eThe variables\u003c/a\u003e just start regular GNOME.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eherbstluftwm\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eNo title bars\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003ei3\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eIceWM\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e➖\u003c/td\u003e\u003ctd\u003eOnly doesn't work for Flatpaks because \u003ca href=\"https://github.com/ice-wm/icewm/blob/b843f88f4baada71ac3c7073b61288e69933f7c8/src/yprefs.h#L37-L42\"\u003eit uses a hardcoded list of icon paths rather than \u003ccode\u003e$XDG_DATA_DIRS\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003eKDE (Plasma)\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003eTaskbar (but not window) falls back on the icon from the Desktop Entry file the app was launched with\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eLXDE\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eLXQt\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eMATE\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eTitle bars have no icons\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eMWM\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eNotion\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eNo title bars\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eOpenbox\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"good\"\u003e\n\t\t\t\t\u003cth\u003ePantheon\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003ePekWM\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eQtile\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eNo title bars\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eStumpwm\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eDid not start\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eSway\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/swaywm/sway/issues/4882\"\u003eArchitected in a way that made icons too complex to bother with.\u003c/a\u003e Might get easier once they take a look at the \u003ccode\u003exdg-toplevel-icon\u003c/code\u003e protocol.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003etwm\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eUKUI\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eWindow decorations and taskbar didn't work\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003eWeston\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eOnly supports client-side decorations\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"mid\"\u003e\n\t\t\t\t\u003cth\u003eXfce\u003c/th\u003e\u003ctd\u003e✔️\u003c/td\u003e\u003ctd\u003e➖\u003c/td\u003e\u003ctd\u003eTaskbar only supports manually transferred icons. Scaling of class-derived icons in title bars is broken.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\u003ctr class=\"bad\"\u003e\n\t\t\t\t\u003cth\u003exmonad\u003c/th\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eNo title bars\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\n\t\u003cfigcaption\u003e\n\t\tI tested all window managers, compositors, and/or desktop environments at their latest version as of January 2025 in their default configuration. There were no differences between the X11 and Wayland versions for the ones that offer both.\u003cbr\u003e\n\t\tYes, you can probably rice title bars and icons onto WMs that don't have them by default. I don't have the time.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's only 6 out of 33 window managers with a bug-free implementation of class-derived icons, and still 6 out of 28 if we disregard all the tiling window managers where icons are not in scope. If you actually want icons in the title bar, the number drops to just 2, KDE and Pantheon. I'm really impressed by IceWM there though, beating all other similarly old and minimal window managers by shipping with an \u003ci\u003ealmost\u003c/i\u003e correct implementation.\u003cbr\u003e\n\tFor now, we'll stay with class-derived icons for budget reasons, but \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/77\"\u003ewe could add a pixel transfer solution in the future\u003c/a\u003e. And that was the 2,000-word story behind \u003ca href=\"https://github.com/nmlgc/ssg/blob/f50ada7370622a5912e24386e931b906cc307ca5/MAIN/main_sdl.cpp#L51\"\u003ethis single line of code\u003c/a\u003e… 📕\n\u003c/p\u003e\u003chr id=\"arch-2025-01-25\"\u003e\u003cp\u003e\n\tOn to packaging then, starting with Arch! Writing my first PKGBUILD was a breeze; as you'd expect from the Arch Wiki, the format and process are very well documented, and the AUR provides tons of examples in case you still need any.\u003cbr\u003e\n\tThe PKGBUILD guidelines have \u003ca href=\"https://wiki.archlinux.org/title/VCS_package_guidelines#Git_submodules\"\u003esome opinions about how to handle submodules\u003c/a\u003e, but applying them would complicate the PKGBUILD quite a bit while bringing us nowhere close to the \u003ca href=\"/blog/2024-03-09#libs-2024-03-09\"\u003e📝 nirvana of shallow and sparse submodules\u003c/a\u003e I've scripted earlier. But since PKGBUILDs are just shell scripts that can naturally call other shell scripts, we can just ignore these guidelines, run \u003ccode\u003ebuild.sh\u003c/code\u003e, and end up with a simpler PKGBUILD and the intended shorter and less bloated package creation process.\n\u003c/p\u003e\u003cp\u003e\n\tSadly, PKGBUILDs don't easily support specifying a dependency on either one of two packages, which we would need to codify the font situation. Due to the way the AUR packages both IPAMonaGothic and MS Gothic together with their \u003ca href=\"https://blog.btrax.com/japanese-type-classifications/\"\u003eMincho\u003c/a\u003e and proportional variants, either of them would be Shuusou Gyoku's largest individual dependency. So you'd only want to install one or the other, but probably not both. We could resolve this by editing the PKGBUILDs of both font packages and adding a \u003ccode\u003eprovides\u003c/code\u003e entry for a new and potentially controversial virtual package like \u003ccode\u003ettf-japanese-14-and-16-pixel-bitmap\u003c/code\u003e that Shuusou Gyoku could then depend on. But with both of the packages being exclusive to the AUR, this dependency would still be annoying to resolve \u003ci\u003eand\u003c/i\u003e you'd have no context about the difference.\u003cbr\u003e\n\tThus, the best we can do is to turn both MS Gothic and IPAMonaGothic into optional dependencies with a short one-line description of the difference, and elaborating on this difference in a comment at the top of the PKGBUILD. Thankfully, the culture around Arch makes this a non-issue because you can reasonably expect people to read your PKGBUILD if they build something from the AUR to begin with. \u003ca href=\"https://www.reddit.com/r/archlinux/comments/8x0p5z/\"\u003eYou do always read the PKGBUILD, right?\u003c/a\u003e \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr id=\"flatpak-2025-01-25\"\u003e\u003cp\u003e\n\tFlatpak, on the other hand… I'm not at all opposed to the fundamental idea of installing another distro on top of an already existing distro for wider ABI compatibility; heck, Flatpak is basically no different from Wine or WSL in this regard. It's just that this particular ABI-widening distro works in a rather… unnatural way that crosses the border into utter cringe at times.\u003cbr\u003e\n\tThere are enough rants about Flatpak from a user's perspective out there, criticizing the bloat relative to native packages, the security implications of bundling libraries, and the questionable utility of its sandbox. But something I rarely see people talk about is just how awful Flatpak is from a developer's point of view:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cp\u003e\n\tThe documentation is written in this weird way that presents Flatpak and its concepts in complete isolation. Without drawing any connections to previous packaging and dependency management systems you might have worked with, it left a lot of my seemingly basic questions unanswered. While it is important to explain your concepts with example code, the lack of a simple and complete reference of the manifest format doesn't exactly inspire confidence in what you're doing. Eventually, I just resorted to cross-checking features in the \u003ca href=\"https://github.com/flatpak/flatpak-builder/blob/main/data/flatpak-manifest.schema.json\"\u003eJSON Schema\u003c/a\u003e to get a better idea of what's actually possible.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe ABI-expanding distro part of Flatpak is actually called the \u003ccite\u003eFreedesktop platform\u003c/cite\u003e, a currently 680\u0026nbsp;MB large stack of typical GUI application libraries updated once a year. It's accompanied by the \u003ccite\u003eFreedesktop SDK\u003c/cite\u003e containing the matching development libraries and tools in another 1.7\u0026nbsp;GB. As the name implies, this distro is maintained by a separate entity \u003ca href=\"https://freedesktop-sdk.io/\"\u003ewith a homepage that makes the entire thing look deeply self-important and unprofessional\u003c/a\u003e. A blurry 25 FPS logo video, a front page full of spelling mistakes, a big focus on sponsors and events… come on, you have \u003ci\u003eone job\u003c/i\u003e, and it's compiling and packaging a bunch of open-source libraries. Was this a result of the usual corporate move of creating more departments in order to shift blame and responsibility?\u003cbr\u003e\n\tOptics aside, their documentation is even more bizarrely useless. The single bit of actual useful information I was looking for – i.e., \u003ca href=\"https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/master/elements/components.bst?ref_type=heads\"\u003ethe concrete list of packages bundled as part of their runtimes\u003c/a\u003e and \u003ca href=\"https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/tree/master/elements/components?ref_type=heads\"\u003etheir versions\u003c/a\u003e, is best found by going straight to their code repo.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe manifest of a Flatpak app can be written in your preferred lesser evil of the two most popular markup languages: JSON (slightly ugly for humans and machines alike), or YAML, the underspecified mess that uses syntactically significant whitespace while \u003ca href=\"https://stackoverflow.com/a/19976827/5035474\"\u003eoutlawing the closest thing we have to a semantic indentation character\u003c/a\u003e. Oh well, YAML at least supports comments, and we sure sorely need them to justify our bleeding-edge C++ module setup to the Flathub maintainers.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eAdding more dependencies on top of the basic runtime can be done by either using \u003ci\u003eruntime extensions\u003c/i\u003e or \u003ci\u003eBaseApps\u003c/i\u003e. That's two entirely separate concepts that appear to do the same thing on the surface, \u003ca href=\"https://github.com/flatpak/flatpak/issues/4797\"\u003eexcept that you can only have one BaseApp\u003c/a\u003e. The \u003ca href=\"https://docs.flatpak.org/en/latest/dependencies.html#baseapps\"\u003edocumentation\u003c/a\u003e then waffles on and tries to explain both concepts with words that have meaning in isolation but once again answer exactly zero of my questions. Must a BaseApp contain a collection of at least two dependencies or why would anyone ever write the sentence that raises this question? Why do they judge BaseApps to be a \"specialized concept\" without elaborating, as if to suggest that their audience is too dumb to understand them? Why does a page named \u003ccite\u003eDependencies\u003c/cite\u003e document extensions as if I wanted to prepare my own package for extension by others? Why be all weird and require \"extension points\" to be defined when it all just comes down to overlaying another filesystem? Who cares about the special significance of the \u003ccode\u003e.Debug\u003c/code\u003e, \u003ccode\u003e.Locale\u003c/code\u003e, and \u003ccode\u003e.Sources\u003c/code\u003e conventions \u003ci\u003ein the context of dependencies\u003c/i\u003e?\u003cbr\u003e\n\tIn the end, you once again get a clearer idea by simply looking at how existing code uses these concepts. Basically, SDK extensions = build-time dependencies, BaseApps = run-time dependencies, and extension points don't matter at all for our purposes because you can just arbitrarily extend the \u003ccode\u003eorg.freedesktop.Sdk\u003c/code\u003e anyway. 🤷\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eSpeaking of extensions: This exact architectural split between build-time and run-time dependencies is why the \u003ccode\u003eorg.freedesktop.Sdk.Extension.llvm19\u003c/code\u003e extension packages Clang, but \u003ci\u003enot\u003c/i\u003e libc++. When questioned about this omission, one of the maintainers \u003ca href=\"https://github.com/flathub/org.freedesktop.Sdk.Extension.llvm17/issues/6#issuecomment-2080091503\"\u003eresponded with the lamest of excuses\u003c/a\u003e: Copying the library would be \u003cq\u003einconvenient\u003c/q\u003e (for them), and \u003cq\u003esomething we can't even imagine a use case for\u003c/q\u003e. Um, guys? \u003ca href=\"https://en.cppreference.com/w/cpp/compiler_support\"\u003eHere's a table. Compare the color of each cell between GCC and Clang. There's your use case.\u003c/a\u003e\u003cbr\u003e\n\tThankfully, you can \u003ca href=\"https://stackoverflow.com/questions/79055137\"\u003ebuild libc++ without building LLVM as a whole\u003c/a\u003e. Seeing how building libc++ takes basically no time at all compared to the rest of LLVM just raises even more questions about not simply providing some kind of script to copy it over.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eFlatpak stores all data of an app in an app-specific subdirectory under \u003ccode\u003e~/.var/app\u003c/code\u003e, inverting and blatantly violating the \u003ca href=\"https://specifications.freedesktop.org/basedir-spec/latest/\"\u003eXDG Base Directory Specification\u003c/a\u003e. \u003ca href=\"https://github.com/flatpak/flatpak.github.io/issues/191\"\u003eEverybody\u003c/a\u003e \u003ca href=\"https://github.com/flatpak/flatpak/issues/1651\"\u003ehates\u003c/a\u003e \u003ca href=\"https://github.com/flatpak/flatpak/issues/3997\"\u003ethis\u003c/a\u003e, and it's indefensible no matter how you look at it. The \u003ca href=\"https://web.archive.org/web/20190925004614/https://bugzilla.mindrot.org/show_bug.cgi?id=2050\"\u003eOpenSSH excuse of being old and having a well-known standard path that long predates the XDG spec\u003c/a\u003e does not apply to Flatpak at all, and neither does any sandboxing argument. Oh, and if your application ships both a Flatpak and XDG-conforming native packages, it must now add \u003ca href=\"https://github.com/nmlgc/ssg/blob/c117cc186fca0804a49df51321f825d84243f2e9/platform/sdl/path_sdl.cpp#L26-L30\"\u003ea special case for Flatpak if it wants to prevent its own XDG directory names from becoming even uglier\u003c/a\u003e. Still, the Flatpak developers remain stubborn about this choice.\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\t\u003cp\u003eBut wait, what's that? Couldn't you theoretically add\u003c/p\u003e\n\t\t\u003cpre\u003e- --filesystem=xdg-data/myapp\n- --env=XDG_DATA_HOME=~/.local/share/myapp\u003c/pre\u003e\n\t\u003cp\u003eto your manifest's \u003ccode\u003efinish-args\u003c/code\u003e? Too bad that \u003ca href=\"https://github.com/flatpak/flatpak/issues/2413\"\u003eFlatpak deliberately prevents this from working\u003c/a\u003e. Not to mention that the resulting package would fail the \u003ca href=\"https://docs.flathub.org/docs/for-app-authors/linter/#finish-args-unnecessary-xdg-data-subdir-mode-access\"\u003e\u003ccode\u003efinish-args-unnecessary-xdg-data-subdir-mode-access\u003c/code\u003e\u003c/a\u003e lint, which would prevent it from being published on Flathub without \u003ca href=\"https://docs.flathub.org/docs/for-app-authors/linter#exceptions\"\u003eapplying for an exception\u003c/a\u003e.\u003c/p\u003e\n\t\u003c/li\u003e\u003c/ul\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eSpeaking of XDG directories, why do they create the \u003ccode\u003e.flatpak-builder\u003c/code\u003e cache directory in the current working directory and not under \u003ccode\u003e$XDG_CACHE_HOME\u003c/code\u003e where it belongs?\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe \u003ccode\u003emodules\u003c/code\u003e in a Flatpak work in a similarly layered way as the commands in a Dockerfile, causing edits to a lower layer to evict previous builds of all successive layers from the cache. Any tweaking work in the lower layers therefore suffers from the same disruptive workflow you might already know from Docker, where you constantly shift the layers around to minimize unnecessary rebuilds because there's never an optimal order. Will we ever see container bros move on from layers to a proper build graph of the entire system? The stagnation in this space is saddening.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eThe \u003ccode\u003e--ccache\u003c/code\u003e option sort of mitigates the layering by at least caching object files in \u003ccode\u003e.flatpak-builder/ccache\u003c/code\u003e, which reduces repeated C compilation to a mere file copy from the cache to the package. But not only is this option \u003ca href=\"https://github.com/flatpak/flatpak-builder/issues/582\"\u003enot enabled by default\u003c/a\u003e, it also doesn't appear in any of the \u003ccode\u003eflatpak-builder\u003c/code\u003e example command lines in the documentation.\u003cbr\u003e\n\tAlso, it only appears to work with GCC, and \u003ca href=\"https://ccache.dev/manual/4.2.html#config_compiler_type\"\u003esetting \u003ccode\u003eCCACHE_COMPILERTYPE=clang\u003c/code\u003e\u003c/a\u003e seems to have no effect. Fortunately, my investment into C++ modules pays off here as well and keeps compile times decently short.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003e\u003ccode\u003eflatpak-builder\u003c/code\u003e doesn't validate the manifest schema? Misspelled or misplaced properties just silently do nothing?\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eSpeaking of validation, why does \u003ccode\u003eflatpak-builder-lint\u003c/code\u003e take 8 seconds to validate a manifest, even if it just consists of a single line? Sure, it's written in Python, but that's an order of magnitude too slow for even that language.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eNo tab completion for any of the \u003ccode\u003eorg.flatpak.Builder\u003c/code\u003e tools. Sandbox working as designed, I guess 🤷\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eGit submodule handling. Oh my goodness.\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\t\u003cp\u003eFlatpak recursively clones and checks out all of a repository's submodules. \u003ca href=\"https://github.com/flatpak/flatpak/issues/59\"\u003eThis might be necessary for some codebases\u003c/a\u003e, but not for this one: The Linux build doesn't need the SDL submodule, and nothing needs the second miniaudio submodule that the \u003ca href=\"https://github.com/mackron/dr_libs\"\u003edr_libs\u003c/a\u003e use for its testing code. And if these recursive submodules \u003ca href=\"https://github.com/mackron/dr_libs/pull/271\"\u003edidn't opt into shallow clones\u003c/a\u003e, you end up with lots of disk space wasted for no reason; 166.1\u0026nbsp;MiB in our case.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eExcept that it's actually twice that amount. There's the download cache that persists across multiple \u003ccode\u003eflatpak-builder\u003c/code\u003e runs, and then there's the temporary directory the build runs in, which gets a full second clone of the entire tree of submodules. This isn't Windows 8, there are no excuses for not using read-only symlinks.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eNone of this would be too bad if we could just do the same thing we did with Arch, ignore the default or recommended submodule processing, and let our shell script run the show and selectively download and check out the submodules required for the Linux build. But no – the build process of a Flatpak is strictly separated into a download stage and a build stage, and the build stage \u003ci\u003ecannot access the network\u003c/i\u003e. Once again, \u003ca href=\"https://www.reddit.com/r/flatpak/comments/rz36im/\"\u003eFlat\u003ci\u003epak\u003c/i\u003e would have the option to allow build-time network access\u003c/a\u003e, but enabling it would mean \u003ca href=\"https://docs.flathub.org/docs/for-app-authors/requirements#no-network-access-during-build\"\u003eno hosting and discoverability on Flat\u003ci\u003ehub\u003c/i\u003e for you\u003c/a\u003e.\u003cbr\u003e\n\t\tI \u003ci\u003eguess\u003c/i\u003e it makes sense from a security point of view, as reviewers would only have to audit a fixed set of declaratively specified sources rather than all code run by the build commands? But even this can only ever apply to the initial review. Allowing app developers to push updates independently from the Flathub maintainers is one of Flathub's biggest selling points. Once you're in, you or your supply chain can just simply hide the malware in an updated version of a module source. 🤷\u003c/p\u003e\n\t\u003c/li\u003e\u003c/ul\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eGetting Tup to work within the Flatpak build environment is slightly tricky. The build sandbox doesn't provide access to the kernel's FUSE module, which Tup uses to track syscalls by default. Thankfully, Tup also supports syscall tracking via \u003ccode\u003eLD_PRELOAD\u003c/code\u003e, which allows us to still build Shuusou Gyoku in a parallelized way with a regular Tup binary. Imagine compiling FUSE from source only to make Tup compile, but then having to build the game via a \u003ccode\u003etup generate\u003c/code\u003ed single-threaded shell script…\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eOne common user complaint about Flatpak is that it allows Windows app developers to stick to their \u003cq\u003ebeloved\u003c/q\u003e and un-Linux-y way of bundling all dependencies, as if they actually ever enjoyed doing that. In reality, it's not the app authors, but the Flathub maintainers and submission reviewers who do everything in their power to prevent Flathub from turning into a typical package manager. Since they ended up with a system where every new extension to the Freedesktop SDK somehow places a burden on the maintainers, they're quick to \u003ca href=\"https://github.com/flathub/flathub/pull/5989\"\u003eshut down everything they consider a bad idea\u003c/a\u003e, \u003ca href=\"https://github.com/flathub/flathub/pull/6000\"\u003eincluding a Tup package I submitted\u003c/a\u003e. What a great job for people who always wanted to be gatekeepers and arbiters of good ideas. If your system treats CMake as one of two blessed build systems that get first-class support, we already fundamentally disagree on basic questions of good taste.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eBecause even the build stages of individual modules are sandboxed from each other, the only way to persist a module's build outputs for further modules is by installing them into the same \u003ccode\u003e/app/\u003c/code\u003e path that the final application is supposed to live in. Since most of these foundational modules will be libraries, \u003ccode\u003e/app/\u003c/code\u003e will be full of C header files, static library files, and library-related tooling that you don't want to bloat your shipped package. Docker solves this with \u003ci\u003emulti-stage builds\u003c/i\u003e: After building your app into an image full of all build-time dependencies and other artifacts vomited out by your build system, you can start from a fresh, minimal base image and selectively copy over only the files your app actually needs to run. Flatpak solves this in the opposite way, merely letting you \u003ca href=\"https://docs.flatpak.org/en/latest/manifests.html#cleanup\"\u003emanually clean up after your dependencies in the end\u003c/a\u003e. At least they support wildcards…\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eSo you've built your Flatpak, but it has an issue that your native build doesn't have and it's time for some debugging. You \u003ca href=\"https://docs.flatpak.org/en/latest/debugging.html#debug-shell\"\u003eopen up a shell into the image\u003c/a\u003e, fire up gdb… and don't get debug symbols despite your build definitely emitting them. The documentation mentions that debug symbols are placed into a separate package, just like Arch Linux's \u003ccode\u003emakepkg\u003c/code\u003e does it, \u003ca href=\"https://docs.flatpak.org/en/latest/debugging.html#debug-packages\"\u003ebut the suggested command line to install them doesn't work\u003c/a\u003e:\n\u003c/p\u003e\u003cblockquote style=\"color: unset\"\u003e\u003ccode\u003e\u003cspan style=\"color:red\"\u003eerror:\u003c/span\u003e No remote refs found for ‘$FLATPAK_ID’\u003c/code\u003e\u003c/blockquote\u003e\u003cp\u003e\n\tThe apparently correct command line can only be found in \u003ca href=\"https://blog.vmsplice.net/2022/04/debugging-flatpak-applications.html\"\u003ethird-party blog posts\u003c/a\u003e. Pulling the package directly out of the builder cache is as random as it gets for someone not deeply familiar with the system.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eBefore you publish your package, you might want to inspect the bundle to make sure that your \u003ccode\u003e--cleanup\u003c/code\u003e entries actually covered all the library bloat you suddenly have to care about. Flatpak also adds a few slight annoyances there:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\tYou could look into the build directory (not the repo directory! Very important difference! 🤪) you pass to \u003ccode\u003eflatpak-builder\u003c/code\u003e, but it also contains all the debug files and source code.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tYou could open the \u003ca href=\"https://docs.flatpak.org/en/latest/debugging.html#debug-shell\"\u003e\u003ccode\u003e--devel\u003c/code\u003e shell\u003c/a\u003e and inspect the contents of \u003ccode\u003e/app/\u003c/code\u003e. This shell environment is rather minimal and misses both a lot of typical Linux userland tools and (of course) a package manager, but \u003ccode\u003els\u003c/code\u003e and \u003ccode\u003efind\u003c/code\u003e work and can do the job.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tThe ideal solution would read explicitly and only from the bundle file. But Flatpak provides no help in this regard, leaving you to resort to \u003ca href=\"https://github.com/flatpak/flatpak/issues/126#issuecomment-227068860\"\u003elow-level hacks that work on the physical container format\u003c/a\u003e. Where's the \u003ca href=\"https://github.com/wagoodman/dive\"\u003eDive\u003c/a\u003e counterpart?\n\t\u003c/li\u003e\u003c/ul\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eSo if all of Flatpak feels like Docker anyway, why isn't it built on top of Docker to begin with? Instead, we got what amounts to a worse copy that doesn't innovate in any way I can notice. Why throw away compatibility with all of Docker's existing tooling just to gain \u003ca href=\"https://blogs.gnome.org/alexl/2017/10/02/on-application-sizes-and-bloat-in-flatpak/\"\u003ehash-based deduplication at the file level for a couple of images\u003c/a\u003e? How can they seriously use a tagline like \u003ca href=\"https://docs.flatpak.org/en/latest/under-the-hood.html#git-for-apps\"\u003e\"Git for apps\"\u003c/a\u003e, which only makes sense for very, \u003ci\u003every\u003c/i\u003e loose definitions of \"Git\"?\u003cbr\u003e\n\tOr maybe all the innovation went into the portals that make this thing work at all, and have at least this little game work indistinguishably from a native build past the initial load time…\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003e… except when parts of it don't! 🤣 Audio is only supported through PulseAudio, which you might not have installed on Arch Linux. Thus, Flatpak ironically enforces another dependency on the host system that the app itself might not have needed.\u003c/p\u003e\n\u003c/li\u003e\u003cli\u003e\n\t\u003cp\u003eAlright, you've submitted your app, incorporated the changes requested by the reviewers, waited a while, and now your app is live and has its own page on Flathub. You'd think I'd be done ranting at this point, but no:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\t\u003cp\u003eYou give them nice lossless PNG screenshots and icons, and they convert both of them to lossy WebP with clearly visible compression artifacts. How about some trust in the fact that people who give you small PNG files know what they're doing? Verified by a programmatic check whether such a lossy recompression even noticeably improves the file size, instead of blindly blowing up our icon to 4.58× the size of the original PNG. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Source-quality images are way more important to me than \u003ca href=\"https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines/#brand-colors\"\u003ebrand colors\u003c/a\u003e.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eThe screenshot area on the app pages has a fixed height of 468 pixels. Is this some kind of a sick joke? How could anyone look at that height and not go \u003ci\u003e\"nah, that looks wrong, 12 more pixels and we'd be VGA-compatible, barely makes a difference anyway\"\u003c/i\u003e?\u003cbr\u003e\n\t\tThat leaves us with two choices:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\t\t\tCrop those 12 pixels out of the raw game screenshots I originally wanted to have there, or\n\t\t\u003c/li\u003e\u003cli\u003e\n\t\t\tfollow \u003ca href=\"https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines/#include-window-shadow-and-rounded-corners\"\u003etheir preferred approach of screenshotting the entire window with its native decorations, rounded corners, and shadows\u003c/a\u003e, and hope the contents still look somewhat presentable when scaled down.\n\t\t\u003c/li\u003e\u003c/ul\u003e\n\t\t\u003cp\u003eThe latter probably isn't the worst idea as it also gives us a chance to show off the \u003cimg\n\t\t\tclass=\"inline_sprite\" src=\"/blog/static/2025-01-25-TremoloMeasure-016.png?b4ffeada\" width=\"16\" height=\"16\" alt=\"\"\n\t\t\u003e 16×16 variant of the icon at its intended size. But I sure didn't immediately find a KDE theme that both has 16-pixel window icons (unlike Breeze's 15 pixels at the \u003ci\u003eSmall\u003c/i\u003e size) and doesn't have obscenely large and asymmetric shadows (unlike Materia or Klassy). Shoutout to the \u003ca href=\"https://github.com/PapirusDevelopmentTeam/arc-kde?tab=readme-ov-file\"\u003eArc theme\u003c/a\u003e for matching all these constraints!\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eMight as well try converting these images to lossless WebP while I'm at it, in the hope that they then leave them alone… but nope, they still get lossily recompressed! 🤪 You know what, I'm not gonna bother with the rest of their guidelines, this is an embarrassment.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eWhy does Flathub claim that the game can access the microphone? I don't remember opting into that. Once again, PulseAudio is to blame, as its security model isn't fine-grained enough. If your app wants to play sound, it has to request access to the PulseAudio socket, which always covers both output and input. \u003ca href=\"https://github.com/widelands/widelands/issues/6477\"\u003eEverybody\u003c/a\u003e \u003ca href=\"https://github.com/flatpak/flatpak/issues/6082\"\u003ehates\u003c/a\u003e \u003ca href=\"https://github.com/flatpak/flatpak/issues/3425\"\u003ethis\u003c/a\u003e, but \u003ca href=\"https://github.com/flatpak/xdg-desktop-portal/discussions/1142\"\u003eit's only going to be fixed with PipeWire \u003ci\u003eand\u003c/i\u003e once the XDG developers have agreed on an audio portal\u003c/a\u003e.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eFinally, game controller support comes with a very similar asterisk. By default, it's disabled just like any other piece of hardware, and \u003ca href=\"https://docs.flatpak.org/en/latest/sandbox-permissions.html#device-access\"\u003ethe documentation\u003c/a\u003e tells you to specify \u003ccode\u003e--device=input\u003c/code\u003e to activate it. However, this specific permission is \u003ca href=\"https://github.com/flatpak/flatpak/pull/5481\"\u003ea fairly recent development in Flatpak terms\u003c/a\u003e and thus isn't widely available yet? Therefore, \u003ca href=\"https://github.com/flathub/flathub/pull/6052#discussion_r1922945616\"\u003ethe reviewers don't yet allow it in manifests\u003c/a\u003e, and your only alternative is a blanket permission for all devices in the user's system. But then, Flathub lists your app as having \u003cq\u003epotentially unsafe user device (and even webcam!) access\u003c/q\u003e, even though you had no alternative except for disabling game controller support. What a nice sandbox they have there… 🙄\u003c/p\u003e\n\t\u003c/li\u003e\u003c/ul\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tIf that's the supposed future of shipping programs on Linux, they've sure made this dev look back into the past with newfound fondness. I'm now more motivated than ever to separately package Shuusou Gyoku for every distribution, if only to see whether there's just a \u003ci\u003esingle\u003c/i\u003e distro out there whose packaging system is worse than Flatpak. But then again, packaging this game for other distros is one of the most obvious \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/contribution-ideas\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/a\u003e\u003c/span\u003e there is.\u003cbr\u003e\n\tIn the end though, the fact that we need to patch Pango to correctly render MS Gothic means that there is a point to shipping Shuusou Gyoku as a Flatpak, beyond just having a single package that works on every distro. And with a download size of 3.4\u0026nbsp;MiB and an installed size of 6.4\u0026nbsp;MiB, Shuusou Gyoku almost exemplifies the ideal use case of Flatpak: Apart from miniaudio, BLAKE3, the IPAMonaGothic font, the temporary libc++, and the patched Pango, all other dependencies of the Linux port happen to be part of the Freedesktop runtime and don't add more bloat to the system.\n\u003c/p\u003e\u003chr id=\"future-2025-01-25\"\u003e\u003cp\u003e\n\tAnd so, we finally have a 100% native Linux port of Shuusou Gyoku, working and packaged, after 36 pushes! 🎉 But as usual, there's always that last bit of optional work left. The three biggest remaining portability gaps are\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ethe 8-bit render path, \u003ca href=\"/blog/2024-10-22#palettized-2024-10-22\"\u003e📝 as I've explained when I ported the graphics\u003c/a\u003e,\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/76\"\u003eguaranteed support for ARM CPUs\u003c/a\u003e, which currently fail to build the project on Flathub due to a Tup issue, and who knows what other issues there might be,\u003c/li\u003e\n\t\u003cli\u003ethe aforementioned proper icon support, and\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/75\"\u003eMIDI playback\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp id=\"midi-2025-01-25\"\u003e\n\tDespite \u003ca href=\"/blog/2024-03-09\"\u003e📝 spending 10 pushes on accurate waveform BGM\u003c/a\u003e, MIDI support seems to be the most worthwhile feature out of the three. The whole point of the BGM work was that Linux doesn't have a native MIDI synth, so why should packagers or even the users themselves jump through the hoops of setting up \u003ci\u003esome\u003c/i\u003e kind of softsynth if it most likely won't sound remotely close to a SC-88Pro? But \u003ca href=\"https://twitter.com/Koto_Sumire/status/1874932509424881866\"\u003eif you already did\u003c/a\u003e, the lack of support might indeed seem unexpected.\u003cbr\u003e\n\tBut as described in the issue, \u003cq\u003eMIDI support\u003c/q\u003e can also mean \"a Windows-like plug-and-play\" experience, without downloading a BGM pack. Despite the resulting \u003cq\u003eunauthentic\u003c/q\u003e sound, this might also be a worthwhile thing to fund if we consider that 14 of the 17 YouTube channels that have uploaded Shuusou Gyoku videos since P0275 still had MIDI playing through the Microsoft GS Wavetable Synth and didn't bother to set up a BGM pack.\n\u003c/p\u003e\u003cp id=\"ipapatch-2025-01-25\"\u003e\n\tFinally, we might want to patch IPAMonaGothic at some point down the line. While a fix for the ascent and descent values that achieves perfect glyph placement without relying on hinting hacks would merely be nice to have, matching the Unicode coverage of its embedded bitmaps with MS Gothic will be crucial for non-ASCII Latin script translations. IPAMonaGothic's outlines do cover \u003ca href=\"https://en.wikipedia.org/w/index.php?title=Latin-1_Supplement\u0026oldid=1254154661#Compact_table\"\u003ethe entire Latin-1 Supplement block\u003c/a\u003e, but the font is missing embedded bitmaps for all of this block's small letters. Since the existing outlines prevent any glyph fallback in both Fontconfig and GDI, letters like ä, ö, ü, and ñ currently render as spaces.\n\u003c/p\u003e\u003cfigure style=\"width: 929px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-FontForge-MS-Gothic-Latin1-14px.png?4e214470\"\n\t\tdata-title=\"MS Gothic\"\n\t\twidth=\"929\"\n\t\tstyle=\"max-height: unset;\"\n\t\talt=\"FontForge screenshot of MS Gothic's embedded 7×14px glyphs in the Basic Latin and Latin-1 Supplement blocks, showing full coverage of both blocks\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2025-01-25-FontForge-IPAMonaGothic-Latin1-14px.png?fa653422\"\n\t\tdata-title=\"IPAMonaGothic\"\n\t\tclass=\"active\"\n\t\twidth=\"929\"\n\t\tstyle=\"max-height: unset;\"\n\t\talt=\"FontForge screenshot of IPAMonaGothic's embedded 7×14px glyphs in the Basic Latin and Latin-1 Supplement blocks, showing missing small letters in the latter block\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tNot pictured here is the fact that IPAMonaGothic also suffers from Greek and Cyrillic glyphs being full-width, like most Japanese fonts from the Shift-JIS era. If we ever translate Shuusou Gyoku into those scripts, we'd probably just hunt for a different font altogether. But it's not worth going on such a hunt for Latin scripts that are only missing a few special characters.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIdeally, I'd like to apply these edits by modifying the embedded bitmaps in a more controlled, documented, and diffable way and then recompiling the font using a pipeline of some sort. The whole field of fonts often feels impenetrable because the usual editing workflow involves throwing a binary file into a bulky GUI tool and writing out a new binary file, and it doesn't have to be this way. But it looks like I'd have to write key parts of that pipeline myself:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe venerable \u003ca href=\"https://github.com/fonttools/fonttools\"\u003e\u003ccode\u003ettx\u003c/code\u003e\u003c/a\u003e provides no comfort features for embedded bitmaps and simply dumps their binary representation as hex strings.\u003c/li\u003e\n\t\u003cli\u003eThe more modern \u003ca href=\"https://unifiedfontobject.org/\"\u003eUFO format\u003c/a\u003e does specify embedded images, but both of the biggest implementations (\u003ca href=\"https://github.com/robotools/defcon\"\u003edefcon\u003c/a\u003e and \u003ca href=\"https://github.com/fonttools/ufoLib2\"\u003eufoLib2\u003c/a\u003e) just throw away any embedded bitmaps, and thus, the whole selling point of such tools.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThat would increase the price of translations by about one extra push if you all agree that this is a good idea. If not, then we just go for the usual way of patching the .ttf file after all. In any case, we then get to host the edited font at a much nicer place than \u003ca href=\"https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=ttf-ipa-mona\"\u003ethe Wayback Machine\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tBut for now, here's the new build:\n\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0303\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01n-032.png?38cbcd51\" alt=\":sh01n:\" width=\"24\" height=\"24\" \n\t\t\tsrcset=\"/static/emoji-sh01n-016.png?b4ffeada 0.66x, /static/emoji-sh01n-032.png?38cbcd51 1.33x, /static/emoji-sh01n-048.png?67a1addc 2x, /static/emoji-sh01n-128.png?96524b5d 5.33x\"\u003e Shuusou Gyoku P0303 Windows build\u003c/a\u003e (now with the new icon)\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://aur.archlinux.org/packages/seihou-shuusou-gyoku\"\u003eShuusou Gyoku on the AUR\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://flathub.org/apps/net.nmlgc.rec98.sh01\"\u003eShuusou Gyoku on Flathub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up: TH02 bullets! Here's to 2025 bringing less build system and maintenance work and more actual progress.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-02-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-12-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2025-01-25T23:56:48Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-12-04",
      "url": "https://rec98.nmlgc.net/blog/2024-12-04",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-01-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-11-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-12-04\"\u003e\u003ctime datetime=\"2024-12-04T14:29:20Z\"\u003e2024-12-04 14:29\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0298\"\u003eP0298\u003c/a\u003e\n\t\t\tTH04/TH05 finalization (GENSOU.SCR, part 2/3) + Decompilation (High Score view screen)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c879dfc...71f4810\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous], \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bgm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The music behind Touhou. Arguably the core of what motivated ZUN to create this series to begin with.\"\u003ebgm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hidden-content\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Hidden features in the original games.\"\u003ehidden-content\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\n\n\n\n\u003cstyle\u003e\n\t.scoreupd-2024-12-04 td {\n\t\tfont-family: monospace;\n\t}\n\t.scoreupd-2024-12-04 b {\n\t\tcolor: red;\n\t}\n\n\t.colors-2024-12-04 {\n\t\tfont-family: monospace;\n\t\tfont-size: min(13px, 2vw);\n\t}\n\t.colors-2024-12-04 tbody th {\n\t\ttext-align: right;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tTH05's \u003ccode\u003eOP.EXE\u003c/code\u003e? It's not one of the \u003ca href=\"/blog/2023-07-28\"\u003e📝 main blockers for multilingual translation support\u003c/a\u003e, but fine, let's push it to 100% RE. This didn't go all \u003ci\u003etoo\u003c/i\u003e quickly after all, though – sure, we were \u003ci\u003eonly\u003c/i\u003e missing the High Score viewer, but that's technically a \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/menu\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/a\u003e\u003c/span\u003e. By now, we all know the level of code quality we can reasonably expect from ZUN's menu code, especially if we simultaneously look at how it's implemented in TH04 as well. But how much could I possibly say about even a static screen?\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#recreate-2024-12-04\"\u003eGenerating the initial High Score lists\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#viewer-2024-12-04\"\u003eTH04/TH05's High Score viewer\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#digits-2024-12-04\"\u003e9-digit scores?\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#limit-2024-12-04\"\u003eThe High Score viewer's actual highest supported score\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#fades-2024-12-04\"\u003eHidden BGM fades\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#palette-2024-12-04\"\u003eTH05-exclusive palette bugs when switching between the main menu and the High Score viewer\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#zunsoft-2024-12-04\"\u003ePlease don't fund ZUN Soft logo finalization just yet 🙇\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"recreate-2024-12-04\"\u003e\u003cp\u003e\n\tThen again, with half of the funding for this push not being constrained to RE, \u003ccode\u003eOP.EXE\u003c/code\u003e wasn't the worst choice. In both TH04 and TH05, the High Score viewer's code is preceded by all the functions needed to handle the \u003ccode\u003eGENSOU.SCR\u003c/code\u003e scorefile format, which I already RE'd \u003ca href=\"/blog/2019-12-28\"\u003e📝 in late 2019\u003c/a\u003e. Back then, it turned out to be one of the most needlessly inconsistent pieces of code in all of PC-98 Touhou, with a slightly different implementation in each of the 6 binaries that was waiting for its equally messy decompilation ever since.\u003cbr\u003e\n\tMost of these inconsistencies just add bloat, but TH05's different stage number defaults for the Extra Stage do have the tiniest visible impact on the game. Since 2019 was \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#labeling-weird-or-broken-code\"\u003ebefore we had our current system of classifying weird code\u003c/a\u003e, let's take a quick look at this again:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH05-GENSOU.SCR-Extra-OP-defaults.png?7d405352\" data-title=\"\u003ccode\u003eOP.EXE\u003c/code\u003e defaults\" width=\"640\" alt=\"Screenshot of TH05's High Score viewer, showing the default GENSOU.SCR data for the Extra Stage as generated by OP.EXE\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH05-GENSOU.SCR-Extra-MAINE-defaults.png?ff5844a9\" data-title=\"\u003ccode\u003eMAINE.EXE\u003c/code\u003e defaults\" width=\"640\" alt=\"Screenshot of TH05's High Score viewer, showing the default GENSOU.SCR data for the Extra Stage as generated by MAINE.EXE with the same decreasing stage numbers as for the regular stages\" class=\"active\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tIn the end, this is a landmine, albeit a slightly unusual one. \u003ccode\u003eOP.EXE\u003c/code\u003e always needs to load \u003ccode\u003eGENSOU.SCR\u003c/code\u003e to determine whether the Extra Stage is unlocked and can be selected in the main menu. If that file is corrupted or doesn't exist yet, \u003ccode\u003eOP.EXE\u003c/code\u003e will always recreate it. Therefore, \u003ccode\u003eMAINE.EXE\u003c/code\u003e's recreation code would only ever run if \u003ccode\u003eGENSOU.SCR\u003c/code\u003e got deleted or corrupted while playing the game. This can only happen through code that runs outside the game or as the result of failing hardware, and thus goes beyond our \u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#observable\"\u003ecriteria for observability\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"viewer-2024-12-04\"\u003e\u003cp\u003e\n\tOn to the actual High Score screen then! The \u003ccode\u003eOP.EXE\u003c/code\u003e code I decompiled here only covers the viewer, the actual score registration is part of \u003ccode\u003eMAINE.EXE\u003c/code\u003e and is a completely different beast that only shares a few code snippets at best. This means that I'll have to do this all over again at some point down the line, which will result in another few pushes that look very similar to this one. 🥲\u003cbr\u003e\n\tBy now, it's no surprise that even this static screen has more or less the same density of bugs, landmines, and bloat as ZUN's more dynamic and animated menus. This time however, the worst source of bloat lies more on the meta level: TH04's version explicitly spells out every single loading and rendering call for both of that game's playable characters, rather than covering them with loops like TH05 does for its four characters. As a result, the two games only share 3¼ out of the 7 functions in even this simple viewer screen. It definitely didn't have to be this way.\n\u003c/p\u003e\u003cp id=\"digits-2024-12-04\"\u003e\n\tOn the bright side, the code starts off with a feature that probably only scoreplayers and their followers have been consciously \u003ca href=\"https://youtu.be/zPJUWqawoQM?si=5CGMh6bAqy53WLhI\u0026t=1675\"\u003eaware\u003c/a\u003e \u003ca href=\"https://www.youtube.com/watch?v=ERi0OVz3PbA\u0026t=787s\"\u003eof\u003c/a\u003e: The High Score screens can display 9-digit scores without glitches, unlike the in-game HUD's infamous overflow that turns the \u003cspan class=\"hovertext\" title=\"Little-endian, so, the 0th digit corresponds to the one's place.\"\u003e8\u003csup\u003eth\u003c/sup\u003e digit\u003c/span\u003e into a letter once the score exceeds 100 million points.\u003cbr\u003e\n\tTo understand why this is such a surprise, we have to look at how scores are tracked in-game where the glitch \u003ci\u003edoes\u003c/i\u003e happen. This brings us back to the \u003ca href=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\"\u003ebinary-coded decimal\u003c/a\u003e format that the final three PC-98 Touhou games use for their scores, which we didn't have to deal with \u003ca href=\"/blog/2021-12-27\"\u003e📝 for almost three years\u003c/a\u003e. On paper, the fixed-size array of 8 digits used by the three games would leave no room for a 9\u003csup\u003eth\u003c/sup\u003e one, so why don't we get a counterstop at 99,999,999 points, similar to what happens in modern Touhou? Let's look at the concrete example of adding, say, 200,000 points to a score of 99,899,990 points, and step through the algorithm for the most significant four digits:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ctable class=\"numbers scoreupd-2024-12-04\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003e\u003ccode\u003escore\u003c/code\u003e\u003c/th\u003e\n\t\t\u003cth\u003eBCD delta\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e09 09 08 09 09 09 09 00\u003c/td\u003e\n\t\t\u003ctd\u003e+ 00 00 02 \u003cb\u003e00\u003c/b\u003e 00 00 00 00\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e= 09 09 08 \u003cb\u003e09\u003c/b\u003e 09 09 09 00\u003c/td\u003e\n\t\t\u003ctd\u003e+ 00 00 \u003cb\u003e02\u003c/b\u003e 00 00 00 00 00\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e= 09 \u003cb\u003e0A 00\u003c/b\u003e 09 09 09 09 00\u003c/td\u003e\n\t\t\u003ctd\u003e+ 00 \u003cb\u003e00\u003c/b\u003e 02 00 00 00 00 00\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e= \u003cb\u003e0A 00\u003c/b\u003e 00 09 09 09 09 00\u003c/td\u003e\n\t\t\u003ctd\u003e+ \u003cb\u003e00\u003c/b\u003e 00 02 00 00 00 00 00\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e= \u003cb\u003e0A\u003c/b\u003e 00 00 09 09 09 09 00\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\t\tIt sure is neat how ZUN arranged the gaiji font in such a way that the HUD's rendering is an exact visual representation of the bytes in memory… at least for scores between 100,000,000 (\u003ccode\u003eA0000000\u003c/code\u003e) and 159,999,999 (\u003ccode\u003eF9999999\u003c/code\u003e) inclusive.\u003cbr\u003e\n\t\tFormatted as big-endian for easier reading. \u003ca href=\"https://github.com/nmlgc/ReC98/blob/c879dfcbffebb89b40199226a12a35088dbd7a10/th04/main/scoreupd.asm#L236-L255\"\u003eHere's the relevant undecompilable ASM code\u003c/a\u003e, featuring the venerable \u003ca href=\"https://www.felixcloutier.com/x86/aaa\"\u003e\u003ccode\u003eAAA\u003c/code\u003e instruction\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIn other words: The carry of each addition is regularly added to the next digit as if it were binary, and then the next iteration has to adjust that value as necessary and pass along any carry to the digit after that. But once we've reached the most significant digit, there is no way for \u003ci\u003eits\u003c/i\u003e carry to go. So it just stays there, leaving the last digit with a value greater than 9 and effectively turning it from a BCD digit into a regular old 8-bit binary value. This leaves us with a maximum representable score of 2,559,999,999 points (\u003ccode\u003eFF 09 09 09 09 09 09 09\u003c/code\u003e) – and with the scores achieved by current TAS runs being far below that limit in \u003ca href=\"https://www.youtube.com/watch?v=4knT22nsqC0\"\u003eboth\u003c/a\u003e \u003ca href=\"https://tasvideos.org/4738M\"\u003egames\u003c/a\u003e, it's definitely not worth it to bother about rendering that 10\u003csup\u003eth\u003c/sup\u003e score digit anywhere.\u003cbr\u003e\n\tIn the High Score screens, ZUN also zero-padded each score to 8 digits, but only blitted the 9\u003csup\u003eth\u003c/sup\u003e digit into the padding between name and score if it's nonzero. From this code detail alone, we can tell that ZUN was fully aware of ≥100 million points being possible, but probably considered such high scores unlikely enough to not bother rearranging the in-game HUD to support 9 digits. After all, it only \u003ci\u003elooks\u003c/i\u003e like there's plenty of unused space next to the HUD, but in reality, it's tightly surrounded by important VRAM regions on both sides: The 32 pixels to the left provide the much-needed sprite garbage area to support \u003ca href=\"/blog/2023-06-30\"\u003e📝 visually clipped sprites despite master.lib's lack of sprite clipping\u003c/a\u003e, and the 64 pixels to the right are home to the \u003ca href=\"/blog/2023-03-30#tiles-2023-03-30\"\u003e📝 tile source area\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH04-In-game-score-overflow-no-TRAM.png?0ccf1b84\" data-title=\"TRAM hidden\" width=\"640\" alt=\"Constructed screenshot of TH04's in-game layout during the Elly boss fight, showing off how a score of 99,999,999 points would be reduced to the 8-digit 🎝9999999 string, with the PC-98's text layer hidden to reveal the sprite garbage areas and tile source areas that tightly surround the HUD\" class=\"active\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH04-In-game-score-overflow.png?82cb7742\" data-title=\"TRAM shown\" width=\"640\" alt=\"Constructed screenshot of TH04's in-game layout during the Elly boss fight, showing off how a score of 99,999,999 points would be reduced to the 8-digit 🎝9999999 string; with the PC-98's text layer visible, it might seem as if it was no big deal to expand the HUD into render 9-digit scores\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tIt sure wouldn't have been impossible. You could either sacrifice the two tiles that would cover the 9\u003csup\u003eth\u003c/sup\u003e digit in both the \u003ci\u003eHiScore\u003c/i\u003e and \u003ci\u003eScore\u003c/i\u003e row, or – even better – move these tiles under the existing padding space within the HUD. \u003ca href=\"/blog/2023-03-30#diffs-2023-03-30\"\u003e📝 The tile sections of TH04 and TH05 already address their images using raw VRAM addresses\u003c/a\u003e, so this wouldn't have even required an additional tile index→VRAM address lookup table.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd sure enough, ZUN confirms this awareness in TH04's \u003ccode\u003eOMAKE.TXT\u003c/code\u003e:\n\u003c/p\u003e\u003cblockquote lang=\"ja\"\u003e９９９９万でもカンストしませんが、ゲーム中の得点表示、１千万の位が英字\n表記となります。つまり、１００００００００点はＡ０００００００点と表示\nされます。（ネームレジスト画面は普通に表示されます）\nでも、１億点以上出せる人いるのかな。\u003c/blockquote\u003e\u003cp\u003e\n\t(And indeed, \u003ca href=\"https://nylilsa.github.io/#/wr#th04\"\u003ethe first documented legitimate run that crossed 100 million only happened 11 years after the game's release, with Reimu A on Normal difficulty\u003c/a\u003e.)\n\u003c/p\u003e\u003cp id=\"limit-2024-12-04\"\u003e\n\tHowever, the highest score that the High Score screens of both games can display without visual glitches is not 999,999,999, as you would expect from 9 digits, but rather…\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH04-High-Score-highest-glitchless.png?f16efc2b\" data-title=\"TH04\" width=\"640\" alt=\"Screenshot of TH04's High Score viewer showing a score of 959,999,999 points for both Reimu and Marisa, which is the highest score that this screen can display without glitches\" class=\"active\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH05-High-Score-highest-glitchless.png?1fe62795\" data-title=\"TH05\" width=\"640\" alt=\"Screenshot of TH05's High Score viewer showing a score of 959,999,999 points for all characters, which is the highest score that this screen can display without glitches\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e959 million?\u003cbr\u003e\n\t(Also, this 9\u003csup\u003eth\u003c/sup\u003e digit nicely highlights a slight asymmetry in TH04's screen, where Marisa gets 4 fewer pixels of padding between names and scores.)\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhat a weird limit. Regardless of whether \u003ccode\u003eGENSOU.SCR\u003c/code\u003e saves its scores in a sane unsigned 32-bit format or a silly 8-digit BCD one, this limit makes no sense in either representation. In fact, \u003ccode\u003eGENSOU.SCR\u003c/code\u003e goes even further than BCD values, and instead uses… the ID of the corresponding gaiji in the \u003ca href=\"/blog/2020-09-16\"\u003e📝 bold font\u003c/a\u003e? \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tHow cute. No matter how you look at it, storing digits with an added offset of 160 makes no sense:\n\u003cul\u003e\n\t\u003cli\u003eIt's suboptimal for the High Score screens (which want to display scores with the digit sprites from \u003ccode\u003eSCNUM.BFT\u003c/code\u003e and thus have to subtract 160 from every digit),\u003c/li\u003e\n\t\u003cli\u003eit's suboptimal for the \u003ci\u003eHiScore\u003c/i\u003e row in the in-game HUD (which also needs actual digits under the hood for easier comparison and replacement with the current \u003ci\u003eScore\u003c/i\u003e, and rendering just adds 160 \u003ci\u003eagain\u003c/i\u003e), and\u003c/li\u003e\n\t\u003cli\u003eit doesn't even work as obfuscation (with an offset of 160 / \u003ccode\u003e0xA0\u003c/code\u003e, you can always read the number by just looking at the lower 4 bits, and each character/rank section in \u003ccode\u003eGENSOU.SCR\u003c/code\u003e is encrypted with its own key anyway).\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIt does start to explain the 959 million limit, though. Since each digit in \u003ccode\u003eGENSOU.SCR\u003c/code\u003e takes up 1 byte as well, they are indeed limited to a maximum value of \u003ccode\u003e(255\u0026nbsp;-\u0026nbsp;160)\u0026nbsp;=\u003c/code\u003e 95 before they wrap back to 0.\u003cbr\u003e\n\tBut wait. If the game simply subtracts 160 from the gaiji index to get the digit value, shouldn't this subtraction \u003ci\u003ealso\u003c/i\u003e wrap back around from 0 to 255 and recover higher values without issue? The answer is, \u003ca href=\"/blog/2024-10-22#cleanup-2024-10-22\"\u003e📝 again, C's integer promotion\u003c/a\u003e: Splitting the binary value into two digits involves a division by 10, the C standard mandates that a regular untyped \u003ccode\u003e10\u003c/code\u003e is always of type \u003ccode\u003eint\u003c/code\u003e, the \u003ccode\u003euint8_t\u003c/code\u003e digit operand gets promoted to match, and the result is actually negative and thus doesn't even get recognized as a 9\u003csup\u003eth\u003c/sup\u003e digit because \u003ca href=\"https://github.com/nmlgc/ReC98/blob/71f481094259fb42b47e75267c2e89774b268a51/th04/hiscore/view.cpp#L142-L144\"\u003eno negative value is ≥10\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tSo what would happen if we were to enter a score that exceeds this limit? The registration screen in \u003ccode\u003eMAINE.EXE\u003c/code\u003e doesn't display the 9\u003csup\u003eth\u003c/sup\u003e digit and the 8\u003csup\u003eth\u003c/sup\u003e one wraps around. But it still sorts the score correctly, so at least the internal processing seems to work without any problem…\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH04-Regist-999-million.png?6b292217\" data-title=\"TH04\" width=\"640\" alt=\"Screenshot of registering a score of 999,999,999 points in TH04, showing off how such a score would wrap around to 39,999,999 points, despite being sorted correctly.\" class=\"active\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH05-Regist-999-million.png?a18a6983\" data-title=\"TH05\" width=\"640\" alt=\"Screenshot of registering a score of 999,999,999 points in TH05, showing off how such a score would wrap around to 39,999,999 points, despite being sorted correctly.\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\u003ccode\u003e(160\u0026nbsp;+\u0026nbsp;99)\u0026nbsp;=\u0026nbsp;259\u003c/code\u003e, which wraps around to 3, so this makes perfect sense. We'll figure out the exact logic behind the differently colored sprite once RE progress reaches this screen.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut once you try viewing this score, you're instead greeted with VRAM corruption resulting from master.lib's \u003ccode\u003esuper_put()\u003c/code\u003e function not bounds-checking the negative sprite IDs passed by the viewer:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH04-High-Score-999-million.png?60449a8a\" data-title=\"TH04\" width=\"640\" alt=\"Screenshot showing how trying to view a score above 959 million points in TH04's High Score viewer leads to VRAM corruption due to master.lib trying to blit a sprite with a negative ID\" class=\"active\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-12-04-TH05-High-Score-999-million.png?571b6b67\" data-title=\"TH05\" width=\"640\" alt=\"Screenshot showing how trying to view a score above 959 million points in TH05's High Score viewer leads to VRAM corruption due to master.lib trying to blit a sprite with a negative ID\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003chr id=\"fades-2024-12-04\"\u003e\u003cp\u003e\n\tIn a rare case for PC-98 Touhou, the High Score viewer also hides two interesting details regarding its BGM. Just like for the graphics, ZUN also coded a fade-in call for the music. In abbreviated ASM code:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003emov ax, 0000h ; PMD AH=00H (start music playback)\nint 60h\nmov ax, 0280h ; PMD AH=02H (fade in/out)\nint 60h\u003c/pre\u003e\n\t\u003cfigcaption\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/blob/c879dfcbffebb89b40199226a12a35088dbd7a10/libs/kaja/pmddata.doc\"\u003ePMD API documentation is here.\u003c/a\u003e\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHowever, the \u003ccode\u003eAH=02H\u003c/code\u003e fade-in call has no effect because \u003ccode\u003eAH=00h\u003c/code\u003e resets the music volume and would need to be followed by a volume-lowering \u003ccode\u003eAH=19h\u003c/code\u003e call. But even if there was such a call, the fade-in would sound terrible. \u003ccode\u003e80h\u003c/code\u003e corresponds to the fastest possible fade-in speed of -128, which is almost but not \u003ci\u003equite\u003c/i\u003e instant. As such, the fade-in would leave the initial note on each channel muted while the rest of the track fades in very abruptly, which clashes badly with the bass and chord notes you'd expect to hear in the name registration themes of the two games:\n\u003c/p\u003e\u003cfigure class=\"fullres\"\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-12-04-TH04-High-Score-viewer-broken-fade-in.flac?a57d3b89\" data-waveform=\"/blog/static/audio/2024-12-04-TH04-High-Score-viewer-broken-fade-in.png?707aca5a\" preload=\"none\" controls data-title=\"TH04 (\u003cspan lang='ja'\u003e幻想の住人\u003c/span\u003e)\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-12-04-TH05-High-Score-viewer-broken-fade-in.flac?feea4e0f\" data-waveform=\"/blog/static/audio/2024-12-04-TH05-High-Score-viewer-broken-fade-in.png?70d14c18\" preload=\"none\" controls data-title=\"TH05 (\u003cspan lang='ja'\u003e魂の休らむ所\u003c/span\u003e)\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAt least the first issue could have been avoided if PMD's \u003ccode\u003eAH=00h\u003c/code\u003e call took optional parameters that describe the initial playback state instead of relying on these mutating calls later on. After all, it might be entirely possible for a bunch of interrupts to fire between \u003ccode\u003eAH=00h\u003c/code\u003e and these further calls, and if those interrupts take a while, the FM chip might have already played a few samples at PMD's default volume. Sure, Real Mode doesn't stop you from wrapping this sequence in \u003ccode\u003eCLI\u003c/code\u003e and \u003ccode\u003eSTI\u003c/code\u003e instructions to work around this issue, but why rely on even more CPU state mutation when there would have been plenty of free x86 registers for passing more initial state to \u003ccode\u003eAH=00h\u003c/code\u003e?\n\u003c/p\u003e\u003cp\u003e\n\tThe second detail is the complete opposite: It's a fade-out when leaving the menu, it uses PMD's slowest fade speed, and it does work and sound good. However, the speed is \u003ci\u003eso\u003c/i\u003e slow that you typically barely notice the feature before the main menu theme starts playing again. But ZUN hid a small easter egg in the code: After the title screen background faded back in, the games wait for all inputs to be released before moving back into the main menu and playing the title screen theme. By holding any key when leaving the High Score viewer, you can therefore listen to the fade-out for as long as you want.\u003cbr\u003e\n\tAlthough when I said that it \u003ci\u003eworks\u003c/i\u003e, this does not include TH04. \u003ca href=\"/blog/2022-11-30\"\u003e📝 As\u003c/a\u003e \u003ca href=\"/blog/2024-02-03\"\u003e📝 usual\u003c/a\u003e, this game's menus do not address the PC-98's \u003ca href=\"https://github.com/nmlgc/ReC98/commit/8dfc2cd\"\u003ekeyboard scancode quirk with regard to held keys\u003c/a\u003e, causing the loop to break even while the player is still holding a key. There are 21 not yet RE'd input polling calls in TH02 and TH04 that will most certainly reveal similar inconsistencies, are you excited yet? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tBut in TH05, holding a key indeed reveals the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/hidden-content\" title=\"Hidden features in the original games.\"\u003ehidden-content\u003c/a\u003e\u003c/span\u003e of a 37-second fade-out:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-12-04-TH05-High-Score-view-BGM-fadeout.webp?91d5c152\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"2557\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-12-04-TH05-High-Score-view-BGM-fadeout.avi?aeaf5b0b\"\u003e\u003csource src=\"/blog/static/video/av1/2024-12-04-TH05-High-Score-view-BGM-fadeout.webm?4aed1a23\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-12-04-TH05-High-Score-view-BGM-fadeout.webm?3dcb5832\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-12-04-TH05-High-Score-view-BGM-fadeout.webm?5eb0daa9\" type=\"video/webm\"\u003eVideo showing off the full BGM fade-out by holding Esc when quitting out of TH05's High Score viewer, and simultaneously revealing two TH05-exclusive palette quirks. \u003ca href=\"/blog/static/video/zmbv/2024-12-04-TH05-High-Score-view-BGM-fadeout.avi?aeaf5b0b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"2\" data-title=\"Brighter palette?\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"180\" data-title=\"Returned to main menu\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"2189\" data-title=\"Fade-out done\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eI'm holding \u003ckbd\u003eEsc\u003c/kbd\u003e here, but this works with any key, even the ⬅️ left and ➡️ right arrow keys that don't quit out of the menu.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"palette-2024-12-04\"\u003e\u003cp\u003e\n\tAs you can already tell by the markers, the final bugs in TH05's (and only TH05's) \u003ccode\u003eOP.EXE\u003c/code\u003e are palette-related and revealed by switching between these two screens:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eWhy does the title screen initially use an ever so slightly darker palette than it does when returning from the menu?\u003c/li\u003e\n\t\u003cli\u003eWhat's with the sudden palette change between frames 1 and 2? Why are the colors suddenly much brighter?\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\t1) is easily traced and attributed to an \u003ca href=\"https://github.com/nmlgc/ReC98/blob/71f481094259fb42b47e75267c2e89774b268a51/th05/op/title.cpp#L89-L91\"\u003eoff-by-one error in the animation's palette fade code\u003c/a\u003e, but 2) is slightly more complex. This palette glitch only happens if the High Score viewer is the first palette-changing submenu you enter after the \u003ca href=\"/blog/2023-11-30#title5-2023-11-30\"\u003e📝 title animation\u003c/a\u003e. Just like \u003ca href=\"/blog/2024-11-22#select-2024-11-22\"\u003e📝 TH03's character portraits\u003c/a\u003e, both TH04 and TH05 load the sprites for the High Score screen's digits (\u003ccode\u003eSCNUM.BFT\u003c/code\u003e) and rank indicator (\u003ccode\u003eHI_M.BFT\u003c/code\u003e) as soon as the title animation has finished. Since these are regular BFNT sprite sheets, ZUN loads them using master.lib's \u003ccode\u003esuper_entry_bfnt()\u003c/code\u003e, and that's where the issue hides: master.lib's blocking palette fade functions operate on master.lib's main 8-bit palette, and \u003ccode\u003esuper_entry_bfnt()\u003c/code\u003e overwrites this palette with the one in the BFNT header. Synchronizing the hardware palette with this newly loaded one would have immediately revealed this possibly unintended state mutation, but I get why master.lib might not have wanted to do that – after all, \u003ca href=\"/blog/2024-11-22#tearing-2024-11-22\"\u003e📝 palette uploads aren't exactly cheap\u003c/a\u003e and would be very noticeable when loading multiple sprite sheets in a row.\u003cbr\u003e\n\tIn any case, this is no problem in TH04 as that game's \u003ccode\u003eHI_M.BFT\u003c/code\u003e and \u003ccode\u003eOP1.PI\u003c/code\u003e have identical palettes. But in TH05, \u003ccode\u003eHI_M.BFT\u003c/code\u003e has a significantly brighter palette:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"colors-2024-12-04\"\u003e\n\t\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\u003cth\u003e0\u003c/th\u003e\n\t\t\u003cth\u003e1\u003c/th\u003e\n\t\t\u003cth\u003e2\u003c/th\u003e\n\t\t\u003cth\u003e3\u003c/th\u003e\n\t\t\u003cth\u003e4\u003c/th\u003e\n\t\t\u003cth\u003e5\u003c/th\u003e\n\t\t\u003cth\u003e6\u003c/th\u003e\n\t\t\u003cth\u003e7\u003c/th\u003e\n\t\t\u003cth\u003e8\u003c/th\u003e\n\t\t\u003cth\u003e9\u003c/th\u003e\n\t\t\u003cth\u003e10\u003c/th\u003e\n\t\t\u003cth\u003e11\u003c/th\u003e\n\t\t\u003cth\u003e12\u003c/th\u003e\n\t\t\u003cth\u003e13\u003c/th\u003e\n\t\t\u003cth\u003e14\u003c/th\u003e\n\t\t\u003cth\u003e15\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr style=\"border-bottom: var(--table-border);\"\u003e\n\t\t\u003cth\u003eOP1.PI\u003c/th\u003e\n\t\t\u003ctd style=\"background-color: #000\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #407\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #707\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #C5C\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #060\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #2A2\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #B00\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #F44\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #00D\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #99F\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #AA4\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FF5\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #EBA\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FEC\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #EAB\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FFF\"\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003cth\u003eHI01.PI / HI_M.BFT\u003c/th\u003e\n\t\t\u003ctd style=\"background-color: #000\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #707\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #909\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #D6D\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #ADA\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #DDD\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #A00\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #F44\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #11D\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #CCF\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #CC6\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FF5\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FCB\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FEC\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FCD\"\u003e\u003c/td\u003e\n\t\t\u003ctd style=\"background-color: #FEE\"\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003chr id=\"zunsoft-2024-12-04\"\u003e\u003cp\u003e\n\tAnd that's 100% RE for TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e! 🎉 TH04's counterpart is not far behind either now, and only misses its title screen animation to reach the same mark.\u003cbr\u003e\n\tAs for 100% \u003ci\u003efinalization\u003c/i\u003e, there's still the not yet decompiled TH04/TH05 version of the ZUN Soft logo that separates both \u003ccode\u003eOP.EXE\u003c/code\u003e binaries from this goal. But as I've mentioned \u003ca href=\"/blog/2023-11-30#main-2023-11-30\"\u003e📝 time and time again\u003c/a\u003e, the most fitting moment for decompiling that animation would be right before reaching 100% on the entirety of either game. Really – as long as we aren't there, your funding is better invested into literally anything else. The ZUN Soft logo does not interact with or block work on any other part of the game, and any potential \u003cspan class=\"hovertext\" title=\"Really?\"\u003emodding\u003c/span\u003e should be easy enough on the ASM level.\u003cbr\u003e\n\tBut thankfully, nobody actually scrolls down to the \u003ci\u003eFinalized\u003c/i\u003e section. So I can rest assured that no one will take that moment away from me! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: I'd kinda like to stay with PC-98 Touhou for a little longer, but the current backlog is pulling into too many different directions and doesn't convincingly point toward one goal over any other. TH02 is close, but with an active subscription, it makes more sense to accumulate 3 pushes of funding and then go for that game's bullet system in January. This is why I'm OK with subscriptions exceeding the cap every once in a while, because they do allow me to plan ahead in the long term.\u003cbr\u003e\n\tSo, let's wait a few days for all of you to capture the open \u003cscript\u003eformatCurrency(11000)\u003c/script\u003e\u003cnoscript\u003e110.00\u0026nbsp;€\u003c/noscript\u003e towards something more specific. But if the backlog stays as indecisive as it is now, I'll instead go for finishing the Shuusou Gyoku Linux port, hopefully in time for the holiday season.\u003cbr\u003e\n\tAs for prices, \u003cscript\u003eformatCurrency(20000)\u003c/script\u003e\u003cnoscript\u003e200.00\u0026nbsp;€\u003c/noscript\u003e indeed seems to be the point where my supply meets the community's demand for this project and the store no longer sells out immediately. So for the time being, we're going to stay at that push price and I won't increase it any further upon hitting the cap.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2025-01-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-11-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-12-04T14:29:20Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-11-22",
      "url": "https://rec98.nmlgc.net/blog/2024-11-22",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-12-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-10-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-11-22\"\u003e\u003ctime datetime=\"2024-11-22T04:00:03Z\"\u003e2024-11-22 04:00\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0296\"\u003eP0296\u003c/a\u003e\n\t\t\tWebsite / Blog (Post list page + Navigation and post header redesign)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/3658763...32d8f54\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0297\"\u003eP0297\u003c/a\u003e\n\t\t\tTH03 decompilation (Character selection screen)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/23fc9e7...c879dfc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"https://www.youtube.com/@32th\"\u003e32th System\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t.point-formula-2024-11-22 {\n\t\tdisplay: inline-grid;\n\t\tgrid-template-rows: 1fr 1fr;\n\t\tvertical-align: middle;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tRemember when ReC98 was about researching the PC-98 Touhou games? After over half a year, we're finally back with some actual RE and decompilation work. The \u003ca href=\"/blog/2024-07-09\"\u003e📝 build system improvement break\u003c/a\u003e was definitely worth it though, the new system is a pure joy to use and injected some newfound excitement into day-to-day development.\u003cbr\u003e\n\tAnd what game would be better suited for this occasion than TH03, which currently has the highest number of individual backers interested in it. Funding the full decompilation of TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e is the clearest signal you can send me that \u003ca href=\"/blog/2024-04-24#integration-2024-04-24\"\u003e📝 you want your future TH03 netplay to be as seamlessly integrated and user-friendly as possible\u003c/a\u003e. We're just two menu screens away from reaching that goal anyway, and the character selection screen fits nicely into a single push.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#select-2024-11-22\"\u003eTH03's character selection screen\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#select-2024-11-22\"\u003eLoading a whole lot of images\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#curves-2024-11-22\"\u003eThe background curve animation\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#perf-2024-11-22\"\u003ePerformance issues\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#issues-2024-11-22\"\u003eBugs and quirks\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#tearing-2024-11-22\"\u003eVRAM and palette tearing\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#website-2024-11-22\"\u003eImproved blog navigability\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"select-2024-11-22\"\u003e\u003cp\u003e\n\tThe code of a menu typically starts with loading all its graphics, and TH03's character selection already stands out in that regard due to the sheer \u003ci\u003eamount\u003c/i\u003e of image data it involves. Each of the game's 9 selectable characters comes with\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003ea 192×192-pixel portrait (\u003ccode\u003e??SL.CD2\u003c/code\u003e),\u003c/li\u003e\n\t\u003cli\u003ea 32×44-pixel pictogram describing her Extra Attack (in \u003ccode\u003eSLEX.CD2\u003c/code\u003e), and\u003c/li\u003e\n\t\u003cli\u003ea 128×16-pixel image of her name (in \u003ccode\u003eCHNAME.BFT\u003c/code\u003e). While this image \u003ci\u003ejust\u003c/i\u003e consists of regular boldfaced versions of font ROM glyphs that the game could just render procedurally, pre-rendering these names and keeping them around in memory does make sense for performance reasons, as we're soon going to see. What \u003ci\u003edoesn't\u003c/i\u003e make sense, though, is the fact that this is a 16-color BFNT image instead of a monochrome one, wasting both memory and rendering time.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tLuckily, ZUN was sane enough to draw each character's stats programmatically. If you've ever looked through this game's data, you might have wondered where the game stores the \u003cimg\n\tclass=\"inline_sprite\"\n\tsrc=\"data:image/gif;base64,R0lGODlhCQAJAPEDAPHxUeGxoYEBAQGB/yH5BAUAAAMALAAAAAAJAAkAAAIZnBenIXnBREsRWEmFBe3paURSF0El4qBDAQA7\"\n\twidth=\"9\"\n\theight=\"9\"\n\talt=\"\"\n\u003e sprite for an individual stat star. There's \u003ccode\u003eSLWIN.CDG\u003c/code\u003e, but that file just contains a full stat window with five stars in all three rows. And sure enough, ZUN renders each character's stats not by blitting sprites, but by painting \u003ccode\u003e(5 - value)\u003c/code\u003e yellow rectangles over the existing stars in that image. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 256px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-11-22-TH03-SLWIN.CDG.png?73684646\" width=\"256\" alt=\"TH03's SLWIN.CDG, showing off how ZUN baked all 15 possible stat stars into the image\"\u003e\n\t\u003cfigcaption\u003eThe only stat-related image you will find as part of the game files. The number of stat stars per character is hardcoded and not based on any other internal constant we know about.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTogether with the \u003cspan style=\"color: #800\"\u003eEXTRA🎔\u003c/span\u003e window and the question mark portrait for Story Mode, all of this sums up to 255,216\u0026nbsp;bytes of image data across 14 files. You could remove the unnecessary alpha plane from \u003ccode\u003eSLEX.CD2\u003c/code\u003e (-1,584 bytes) or store \u003ccode\u003eCHNAME.BFT\u003c/code\u003e in a 1-bit format (-6,912 bytes), but using 3.3% less memory barely makes a difference in the grand scheme of things.\u003cbr\u003e\n\tFrom the code, we can assume that loading such an amount of data all at once would have led to a noticeable pause on the game's target PC-98 models. The obvious alternative would be to just start out with the initially visible images and lazy-load the data for other characters as the cursors move through the menu, but the resulting mini-latencies would have been bound to cause minor frame drops as well. Instead, ZUN opted for a rather creative solution: By segmenting the loading process into four parts and moving three of these parts ahead into the main menu, we instead get four smaller latencies in places where they don't stick out as much, if at all:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eThe loading process starts at the logo animation, with Ellen's, Kotohime's, and Kana's portraits getting loaded after the \u003cspan lang=\"ja\"\u003e\u003cspan style=\"color: #909;\"\u003e東方\u003c/span\u003e\u003cspan style=\"color: #f44;\"\u003e夢\u003c/span\u003e\u003cspan style=\"color: #909;\"\u003e時空\u003c/span\u003e \u003c/span\u003e letters finished sliding in. Why ZUN chose to start with characters #3, #4, and #5 is anyone's guess. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eReimu's, Mima's, and Marisa's portraits as well as all 9 \u003cspan style=\"color: #800\"\u003eEXTRA🎔\u003c/span\u003e attack pictograms are loaded at the end of the flash animation once the full title image is shown on screen and before the game is waiting for the player to press a key.\u003c/li\u003e\n\t\u003cli\u003eThe stat and \u003cspan style=\"color: #800\"\u003eEXTRA🎔\u003c/span\u003e windows are loaded at the end of the main menu's slide-in animation… together with the question mark portrait for Story Mode, even though the player might not actually want to play Story Mode.\u003c/li\u003e\n\t\u003cli\u003eFinally, the game loads Rikako's, Chiyuri's, and Yumemi's portraits after it cleared VRAM upon entering the Select screen, regardless of whether the latter two are even unlocked.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tI don't like how ZUN implemented this split by using three separately named standalone functions with their own copy-pasted character loop, and the load calls for specific files could have also been arranged in a more optimal order. But otherwise, this has all the ingredients of \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e. As usual, though, ZUN then definitively ruins it all by counteracting the intended latency hiding with… deliberately added latency frames:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe entire initialization process of the character selection screen, including Step #4 of image loading, is enforced to take at least 30 frames, with the count starting before the switch to the \u003cspan lang=\"ja\"\u003eSelection\u003c/span\u003e theme. Presumably, this is meant to give the player enough time to release the Z key that entered this menu, because holding it would immediately select Reimu (in Story mode) or the previously selected 1P character (in VS modes) on the very first frame. But this is a workaround at best – and a completely unnecessary one at that, given that regular navigation in this menu \u003ci\u003ealready\u003c/i\u003e needs to lock keys until they're released. In the end, you can still auto-select the default choice by just not releasing the Z key.\u003c/li\u003e\n\t\u003cli\u003eAnd if that wasn't enough, the 1P vs. 2P variant of the menu adds 16 more frames of startup delay on top.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSure, \u003ci\u003emaybe\u003c/i\u003e loading the fourth part's 69,120\u0026nbsp;bytes from a highly fragmented hard drive \u003ci\u003emight\u003c/i\u003e have even taken longer than 30 frames on a period-correct PC-98, but the point still stands that these delays don't solve the problem they are supposed to solve.\n\u003c/p\u003e\u003chr id=\"curves-2024-11-22\"\u003e\u003cp\u003e\n\tBut the unquestionable main attraction of this menu is its fancy background animation. Mathematically, it consists of \u003ca href=\"https://en.wikipedia.org/wiki/Lissajous_curve\"\u003eLissajous curves\u003c/a\u003e with a twist: Instead of calculating each point as\n\t\u003cspan class=\"point-formula-2024-11-22\"\u003e\n\t\t\u003ccode\u003ex = sin((f\u003csub\u003ex\u003c/sub\u003e·t)+ẟ\u003csub\u003ex\u003c/sub\u003e)\u003c/code\u003e\n\t\t\u003ccode\u003ey = sin((f\u003csub\u003ey\u003c/sub\u003e·t)+ẟ\u003csub\u003ey\u003c/sub\u003e)\u003c/code\u003e\n\t\u003c/span\u003e, TH03 effectively calculates its points as\n\t\u003cspan class=\"point-formula-2024-11-22\"\u003e\n\t\t\u003ccode\u003ex = cos(f\u003csub\u003ex\u003c/sub\u003e·((t+ẟ\u003csub\u003ex\u003c/sub\u003e)\u0026nbsp;%\u0026nbsp;0xFF))\u003c/code\u003e\n\t\t\u003ccode\u003ey = sin(f\u003csub\u003ey\u003c/sub\u003e·((t+ẟ\u003csub\u003ey\u003c/sub\u003e)\u0026nbsp;%\u0026nbsp;0xFF))\u003c/code\u003e\n\t\u003c/span\u003e, due to \u003ccode class=\"hovertext\" title=\"constant base angle of the point on the circle\"\u003et\u003c/code\u003e and \u003ccode class=\"hovertext\" title=\"variable and animated angle offset\"\u003eẟ\u003c/code\u003e being \u003ca href=\"/blog/2022-03-05\"\u003e📝 8-bit angles\u003c/a\u003e. Since the result of the addition remains 8-bit as well, it can and will regularly overflow before the frequency scaling factors \u003ccode\u003ef\u003csub\u003ex\u003c/sub\u003e\u003c/code\u003e and \u003ccode\u003ef\u003csub\u003ey\u003c/sub\u003e\u003c/code\u003e are applied, thus leading to sudden jumps between both ends of the 8-bit value range. The combination of this overflow and the gradual changes to \u003ccode\u003ef\u003csub\u003ex\u003c/sub\u003e\u003c/code\u003e and \u003ccode\u003ef\u003csub\u003ey\u003c/sub\u003e\u003c/code\u003e create all these interesting splits along the 360° of the curve:\n\u003c/p\u003e\u003cp\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-11-22-TH03-Select-curves-original.webp?f1fc1766\" preload=\"none\" controls data-title=\"Full animation\" loop data-active width=\"640\" height=\"400\" data-fps=\"18.807710666666665\" data-frame-count=\"128\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-original.avi?bbc710cb\"\u003e\u003csource src=\"/blog/static/video/av1/2024-11-22-TH03-Select-curves-original.webm?bf483437\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-11-22-TH03-Select-curves-original.webm?eba18ffb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-11-22-TH03-Select-curves-original.webm?2b0713d8\" type=\"video/webm\"\u003eVideo of one cycle of TH03's Select screen curve animation. \u003ca href=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-original.avi?bbc710cb\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-11-22-TH03-Select-curves-no-trails.webp?b28460bf\" preload=\"none\" controls data-title=\"No trailing curves\" loop width=\"640\" height=\"400\" data-fps=\"18.807710666666665\" data-frame-count=\"128\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-no-trails.avi?66fb0466\"\u003e\u003csource src=\"/blog/static/video/av1/2024-11-22-TH03-Select-curves-no-trails.webm?4a441e2e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-11-22-TH03-Select-curves-no-trails.webm?6d594964\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-11-22-TH03-Select-curves-no-trails.webm?93232b73\" type=\"video/webm\"\u003eVideo of one cycle of TH03's Select screen animation, without trailing curves. \u003ca href=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-no-trails.avi?66fb0466\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eAt a high level, there really is just one big curve and one small curve, plus an array of trailing curves that approximate motion blur by subtracting from ẟ\u003csub\u003ex\u003c/sub\u003e and ẟ\u003csub\u003ey\u003c/sub\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp id=\"perf-2024-11-22\"\u003e\n\tIn a rather unusual display of mathematical purity, ZUN fully re-calculates all variables and every point on every frame from just the single byte of state that indicates the current time within the animation's 128-frame cycle. However, that beauty is quickly tarnished by the sheer \u003ci\u003ecost\u003c/i\u003e of fully recalculating these curves every frame:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIn total, the effect calculates, clips, and plots 16 curves: 2 main ones, with up to 7×2\u0026nbsp;=\u0026nbsp;14 darker trailing curves.\u003c/li\u003e\n\t\u003cli\u003eEach of these curves is made up of the 256 maximum possible points you can get with 8-bit angles, giving us 4,096 points in total.\u003c/li\u003e\n\t\u003cli\u003eEach of these points takes \u003ci\u003eat least\u003c/i\u003e 333 cycles on a 486 if it passes all clipping checks, not including VRAM latencies or the performance impact of the \u003ca href=\"/blog/2020-12-18\"\u003e📝 GRCG's RMW mode\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eDue to the larger curve's diameter of 440 pixels, a few of the points at its edges are needlessly calculated only to then be discarded by the clipping checks as they don't fit within the 400 VRAM rows. Still, \u003e1.3 million cycles for a single frame remains a reasonable ballpark assumption.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis is decidedly more than the 1.17 million cycles we have between each VSync on the game's target 66\u0026nbsp;MHz CPUs. So it's not surprising that this effect is not rendered at 56.4\u0026nbsp;FPS, but instead drops the frame rate of the entire menu by targeting a hardcoded 1 frame per 3 VSync interrupts, or 18.8\u0026nbsp;FPS. Accordingly, I reduced the frame rate of the video above to represent the actual animation cycle as cleanly as possible.\u003cbr\u003e\n\tApparently, ZUN also tested the game on the 33\u0026nbsp;MHz PC-98 model that he targeted with TH01, and realized that 4,096 points were way too much even at 18.8\u0026nbsp;FPS. So he also added a mechanism that decrements the number of trailing curves if the last frame took ≥5 VSync interrupts, down to a minimum of only a single extra curve. You can see this in action by underclocking the CPU in your Neko Project fork of choice.\n\u003c/p\u003e\u003cp\u003e\n\tBut were any of these measures really necessary? Couldn't ZUN just have allocated a \u003cspan class=\"hovertext\" title=\"2 bytes for the VRAM offset, 1 byte for the dot pattern written using the GRCG\"\u003e12\u0026nbsp;KiB\u003c/span\u003e ring buffer to keep the coordinates of previous curves, thus reducing per-frame calculations to just 512 points? Well, \u003ci\u003ehe\u003c/i\u003e could have, but \u003ci\u003ewe\u003c/i\u003e now can't use such a buffer to optimize the original animation. The 8-bit main angle offset/animation cycle variable advances by \u003ccode\u003e0x02\u003c/code\u003e every frame, but some of the trailing curves subtract odd numbers from this variable and thus fall between two frames of the main curves.\u003cbr\u003e\n\tSo let's shelve the idea of high-level algorithmic optimizations. In this particular case though, even micro-optimizations can have massive benefits. The sheer number of points magnifies the performance impact of every suboptimal code generation decision within the inner point loop:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eInlining \u003ccode\u003egrcg_pset()\u003c/code\u003e would save 42 cycles by cutting out one \u003ccode\u003efar\u003c/code\u003e function call and the required stack and register shuffling for passing two parameters. (Remember how \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 TH01's EGC-powered unblitter suffered from the same function call performance issue\u003c/a\u003e?)\u003c/li\u003e\n\t\u003cli\u003e\n\t\tFrequency scaling works by multiplying the 8-bit angles with a fixed-point Q8.8 factor. The result is then scaled back to regular integers via… two divisions by 256 rather than two bitshifts? That's another ≥46 cycles where ≥10 would have sufficed.\u003cbr\u003e\n\t\t\u003ci\u003e\u003cstrong\u003eEdit (2025-08-29):\u003c/strong\u003e The initial version of this post miscounted the number of required cycles as ≥4, or 2× the cycle count of a single \u003ccode\u003eSAR\u003c/code\u003e instruction. That number didn't consider that the frequency scaling multiplication occasionally produces negative numbers, which \u003ca href=\"/blog/2025-02-24#sar-2025-02-24\"\u003e📝 must be conditionally rounded up\u003c/a\u003e when replacing signed divisions with arithmetic bitshifts to still produce the exact original animation. This conditional rounding adds ≥8 cycles in the more common positive case, and ≥6 in the rarer negative case.\u003c/i\u003e\n\t\u003c/li\u003e\n\t\u003cli\u003eThe biggest gains, however, would come from inlining the two \u003ccode\u003efar\u003c/code\u003e calls to the 5-instruction function that calculates one dimension of a polar coordinate, saving another ≥100 cycles.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tMultiplied by the number of points, even these low-hanging fruit already save a whopping ≥729,088 cycles \u003ci\u003eper frame\u003c/i\u003e on an i486, without writing a single line of ASM! On Pentium CPUs such as the one in the PC-9821Xa7 that ZUN supposedly developed this game on, the savings are slightly smaller because \u003ccode\u003efar\u003c/code\u003e calls are much faster, but still come in at a hefty ≥466,944 cycles. Thus, this animation easily beats \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 TH01's sprite blitting and unblitting code\u003c/a\u003e, which just barely hit the 6-digit mark of wasted cycles, and snatches the crown of being the single most unoptimized code in all of PC-98 Touhou.\u003cbr\u003e\n\tThe incredible irony here is that TH03 is the point where ZUN \u003ca href=\"/blog/2019-11-29\"\u003e📝 really\u003c/a\u003e \u003ca href=\"/blog/2020-11-16\"\u003e📝 started\u003c/a\u003e \u003ca href=\"/blog/2022-02-18\"\u003e📝 going\u003c/a\u003e \u003ca href=\"/blog/2024-04-24#hitcirc-2024-04-24\"\u003e📝 overboard\u003c/a\u003e with useless ASM micro-optimizations, yet he didn't even begin to optimize \u003ci\u003ethe one thing\u003c/i\u003e that would have actually benefitted from it. Maybe he \u003ca href=\"/blog/2022-08-11\"\u003e📝 once again\u003c/a\u003e went for the 📽️ \u003ci\u003ecinematic look\u003c/i\u003e 📽️ on purpose?\n\u003c/p\u003e\u003cp\u003e\n\tUnlike TH01's sprites though, all this wasted performance doesn't really matter much in the end. Sure, optimizing the animation would give us more trailing curves on slower PC-98 models, but any attempt to increase the frame rate by interpolating angles would send us straight into fanfiction territory. Due to the \u003ccode\u003e0x02\u003c/code\u003e/2.8125° increment per cycle, tripling the frame rate of this animation would require a change to a very awkward (log\u003csub\u003e2\u003c/sub\u003e384)\u0026nbsp;= 8.58-bit angle format, complete with a new 384-entry sine/cosine lookup table. And honestly, the effect does look quite impressive even at 18.8\u0026nbsp;FPS.\n\u003c/p\u003e\u003chr id=\"issues-2024-11-22\"\u003e\u003cp\u003e\n\tThere are three more bugs and quirks in this animation that are unrelated to performance:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003eIf you've tried counting the number of trailing dots in the video above, you might have noticed that the very first frame actually renders \u003ci\u003e8\u003c/i\u003e×2 trailing curves instead of \u003ci\u003e7\u003c/i\u003e×2, thus rendering an even higher 4,608 points. What's going on there is that ZUN actually requested 8 trailing curves, but then forgot to reset the VSync counter after the initial 30-frame delay. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e As a result, the game always thinks that the first frame of the menu took ≥30 VSync interrupts to render, thus causing the decrement mechanism to kick in and deterministically reduce the trailing curve count to 7.\u003cbr\u003e\n\tThis is a textbook example of my definition of a ZUN bug: The code unmistakably says 8, and we only don't get 8 because ZUN forgot to mutate a piece of global state.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eThe small trailing curves have a noticeable discontinuity where they suddenly get rotated by ±90° between the last and first frame of the animation cycle.\u003cbr\u003e\n\tThis quirk comes down to the small curve's \u003ccode\u003eẟ\u003csub\u003ey\u003c/sub\u003e\u003c/code\u003e angle offset being calculated as \u003ccode\u003e((c/2)-i)\u003c/code\u003e, with \u003ccode\u003ei\u003c/code\u003e being the number of the trailing curve. Halving the main cycle variable effectively restricts this smaller curve to only the first half of the sine oscillation, between [\u003ccode\u003e0x00\u003c/code\u003e,\u0026nbsp;\u003ccode\u003e0x80\u003c/code\u003e[. For the main curve, this is fine as \u003ccode\u003ei\u003c/code\u003e is always zero. But once the trailing curves leave us with a negative value after the subtraction, the resulting angle suddenly flips over into the second half of the sine oscillation that the regular curve never touches. And if you recall how a sine wave looks, the resulting visual rotation immediately makes sense:\n\t\u003cfigure\u003e\n\t\t\u003cembed src=\"/blog/static/2024-11-22-TH03-Select-small-curve-quirk.svg?47cdcca1\"\u003e\n\t\t\u003cfigcaption\u003eNegated input, negated output.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\tRemoving the division would be the most obvious fix, but that would double the speed of the sine oscillation and change the shape of the curve way beyond ZUN's intentions. The second-most obvious fix involves matching the trailing curves to the movement of the main one by restricting the subtraction to the first half of the oscillation, i.e., calculating \u003ccode\u003eẟ\u003csub\u003ey\u003c/sub\u003e\u003c/code\u003e as \u003ccode\u003e(((c/2)-i)\u0026nbsp;%\u0026nbsp;0x80)\u003c/code\u003e instead. With \u003ccode\u003ec\u003c/code\u003e increasing by \u003ccode\u003e0x02\u003c/code\u003e on each frame of the animation, this fix would only affect the first 8 frames.\u003c/li\u003e\n\t\u003cli\u003eZUN decided to plot the darker trailing curves on top of the lighter main ones. Maybe it should have been the other way round?\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-11-22-TH03-Select-curves-original.webp?f1fc1766\" preload=\"none\" controls data-title=\"Original game\" loop width=\"640\" height=\"400\" data-fps=\"18.807710666666665\" data-frame-count=\"128\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-original.avi?bbc710cb\"\u003e\u003csource src=\"/blog/static/video/av1/2024-11-22-TH03-Select-curves-original.webm?bf483437\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-11-22-TH03-Select-curves-original.webm?eba18ffb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-11-22-TH03-Select-curves-original.webm?2b0713d8\" type=\"video/webm\"\u003eVideo of one cycle of TH03's Select screen curve animation. \u003ca href=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-original.avi?bbc710cb\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"8\" data-title=\"Back in sync after discontinuity fix\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-11-22-TH03-Select-curves-fixed.webp?29f1492c\" preload=\"none\" controls data-title=\"Fixed quirks and bugs\" loop data-active width=\"640\" height=\"400\" data-fps=\"18.807710666666665\" data-frame-count=\"128\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-fixed.avi?08531a5e\"\u003e\u003csource src=\"/blog/static/video/av1/2024-11-22-TH03-Select-curves-fixed.webm?40aca4c7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-11-22-TH03-Select-curves-fixed.webm?57f13331\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-11-22-TH03-Select-curves-fixed.webm?dc60acf6\" type=\"video/webm\"\u003eVideo of one cycle of TH03's Select screen curve animation, with all bugs and quirks fixed. \u003ca href=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-curves-fixed.avi?08531a5e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"8\" data-title=\"Back in sync after discontinuity fix\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eNow with the full 18 curves, a direction change of the smaller trailing curves at the end of the loop that only looks \u003ci\u003eslightly\u003c/i\u003e odd, and a reversed and more natural plotting order.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf you want to play with the math in a more user-friendly and high-res way, \u003ca href=\"https://www.desmos.com/calculator/sstcw9ru5x?invertedColors=true\"\u003ehere's a Desmos graph of the full animation, converted to 360° angles and with toggles for the discontinuity and trail count fixes.\u003c/a\u003e\n\u003c/p\u003e\u003chr id=\"tearing-2024-11-22\"\u003e\u003cp\u003e\n\tNow that we fully understand how the curve animation works, there's one more issue left to investigate. Let's actually try holding the Z key to auto-select Reimu on the very first frame of the Story Mode Select screen:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-11-22-TH03-Select-holding-Shot.webp?beb3e1db\" preload=\"none\" controls width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"146\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-holding-Shot.avi?4944d4c9\"\u003e\u003csource src=\"/blog/static/video/av1/2024-11-22-TH03-Select-holding-Shot.webm?18edcb31\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-11-22-TH03-Select-holding-Shot.webm?f7cadbb1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-11-22-TH03-Select-holding-Shot.webm?7961b434\" type=\"video/webm\"\u003eVideo demonstrating how holding the Z key when entering TH03's Story Mode character select screen auto-selects Reimu after a 30-frame delay. \u003ca href=\"/blog/static/video/zmbv/2024-11-22-TH03-Select-holding-Shot.avi?4944d4c9\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"2\" data-title=\"Delay starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"32\" data-title=\"Confirmation flash\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"97\" data-title=\"?!\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThe confirmation flash even happens before the menu's first page flip.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tStepping through the individual frames of the video above reveals quite a bit of tearing, particularly when VRAM is cleared in frame 1 and during the menu's first page flip in frame 49. This might remind you of \u003ca href=\"/blog/2024-02-03#mess-2024-02-03\"\u003e📝 the tearing issues in the Music Rooms\u003c/a\u003e – and indeed, this tearing is once again the expected result of ZUN landmines in the code, not an emulation bug. In fact, quite the contrary: Scanline-based rendering is a mark of quality in an emulator, as it always requires more coding effort and processing power than not doing it. Everyone's favorite two PC-98 emulators from 20 years ago might look nicer on a per-frame basis, but only because they effectively hide ZUN's frequent confusion around VRAM page flips.\u003cbr\u003e\n\tTo understand these tearing issues, we need to consider two more code details:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eIf a frame took longer than 3 VSync interrupts to render, ZUN flips the VRAM pages immediately without waiting for the next VSync interrupt.\u003c/li\u003e\n\t\u003cli\u003eThe hardware palette fade-out is the last thing done at the end of the per-frame rendering loop, but \u003ci\u003ebefore\u003c/i\u003e busy-waiting for the VSync interrupt.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThe combination of 1) and the aforementioned 30-frame delay quirk explains Frame 49. There, the page flip happens within the second frame of the three-frame chunk while the electron beam is drawing row #156. DOSBox-X doesn't try to be cycle-accurate to specific CPUs, but 1 menu frame taking \u003cspan class=\"hovertext\" title=\"Or 31.39 frames, as far as the game is concerned\"\u003e1.39 real-time frames\u003c/span\u003e at 56.4\u0026nbsp;FPS is roughly in line with the cycle counting we did earlier.\u003cbr\u003e\n\tFrame 97 is the much more intriguing one, though. While it's mildly amusing to see the palette actually go \u003ci\u003ebrighter\u003c/i\u003e for a single frame before it fades out, the interesting aspect here is that 2) practically guarantees its palette changes to happen mid-frame. And since the CRT's electron beam might be anywhere at that point… yup, that's how you'd get more than 16 colors out of the PC-98's 16-color graphics mode. 🎨\u003cbr\u003e\n\tLet's exaggerate the brightness difference a bit in case the original difference doesn't come across too clearly on your display:\n\u003c/p\u003e\u003cfigure class=\"pixelated fullres\"\u003e\n\t\u003cimg src=\"/blog/static/2024-11-22-TH03-Select-exaggerated-mid-frame-palette-change.png?fa838607\" width=\"640\" alt=\"Frame 97 of the video above, with a brighter initial palette to highlight the mid-frame palette change\"\u003e\n\t\u003cfigcaption\u003e\n\t\tProbably not too much of a reason for demosceners to get excited; generic PC-98 code that doesn't try to target specific CPUs would still need a way of reliably timing such mid-frame palette changes. Bit 6 (\u003ccode\u003e0x40\u003c/code\u003e) of I/O port \u003ccode\u003e0xA0\u003c/code\u003e indicates HBlank, and the usual documentation suggests that you could just busy-wait for that bit to flip, but an HBlank interrupt would be much nicer.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis reproduces on both DOSBox-X and Neko Project 21/W, although the latter needs the \u003ci\u003eScreen\u0026nbsp;→ Real palettes\u003c/i\u003e option enabled to actually emulate a CRT electron beam. Unfortunately, I couldn't confirm it on real hardware because my PC-9821Nw133's screen \u003ca href=\"https://en.wikipedia.org/wiki/Vinegar_syndrome\"\u003evinegar'd\u003c/a\u003e at the beginning of the year. But just as with the image loading times, TH03's remaining code sorts of indicate that mid-frame palette changes were noticeable on real hardware, by means of \u003ca href=\"https://github.com/nmlgc/ReC98/commit/1cb2c0acbc532b959feba63de540c474f9840e19\"\u003ethis little flag I RE'd way back in March 2019\u003c/a\u003e. Sure, \u003ccode\u003epalette_show()\u003c/code\u003e takes \u003e2,850 cycles on a 486 to downconvert master.lib's 8-bit palette to the GDC's 4-bit format and send it over, and that might add up with more than one palette-changing effect per frame. But tearing is a way more likely explanation for deferring all palette updates until after VSync and to the next frame.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that completes another menu, placing us a very likely 2 pushes away from completing TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e! Not many of those left now…\n\u003c/p\u003e\u003chr id=\"website-2024-11-22\"\u003e\u003cp\u003e\n\tTo balance out this heavy research into a comparatively small amount of code, I slotted in 2024's Part 2 of my usual bi-annual website improvements. This time, they went toward future-proofing the blog and making it a lot more navigable. You've probably already noticed the changes, but here's the full changelog:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\tThe \u003ci\u003eProgress blog\u003c/i\u003e link in the main navigation bar now points to a new list page with just the post headers and each post's table of contents, instead of directly overwhelming your browser with a view of every blog post ever on a single page.\u003cbr\u003e\n\t\tIf you've been reading this blog regularly, you've probably been starting to dread clicking this link just as much as I've been. 14\u0026nbsp;MB of initially loaded content isn't \u003ci\u003etoo\u003c/i\u003e bad for 136 posts with an increasing amount of media content, but laying out the now 2\u0026nbsp;MB of HTML sure takes a while, leaving you with a sluggish and unresponsive browser in the meantime. The old one-page view is still available \u003ca href=\"/blog/all\"\u003eat a dedicated URL\u003c/a\u003e in case you want to \u003ccode\u003eCtrl-F\u003c/code\u003e over the entire history from time to time, but it's no longer the default.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\tThe new 🔼 and 🔽 buttons now allow quick jumps between blog posts without going through the table of contents or the old one-page view. These work as expected on all views of the blog: On single-post pages, the buttons link to the adjacent single-post pages, whereas they jump up and down within the same page on the \u003ca href=\"/blog\"\u003elist of posts\u003c/a\u003e or the tag-filtered and one-page views.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\tThe header section of each post now shows the individual goals of each push that the post documents, providing a sort of title. This is much more useful than wasting space with meaningless commit hashes; just like in the \u003ca href=\"/fundlog\"\u003elog\u003c/a\u003e, links to the commit diffs don't need to be longer than a GitHub icon.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\tThe web feeds that \u003ca href=\"/blog/2022-09-04\"\u003e📝 handlerug implemented two years ago\u003c/a\u003e are now prominently displayed in the new blog navigation sub-header. Listing them using \u003ccode\u003e\u0026lt;link rel=\"alternate\"\u0026gt;\u003c/code\u003e tags in the HTML \u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e is usually enough for integrated feed reader extensions to automatically discover their presence, but it can't hurt to draw more attention to them. Especially now that Twitter has been locking out unregistered users for quite some time…\n\t\u003c/p\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSpeaking of microblogging platforms, I've now also followed a good chunk of the Touhou community to \u003ca href=\"https://bsky.app/profile/rec98.nmlgc.net\"\u003eBluesky\u003c/a\u003e! The algorithms there seem to treat my posts \u003ca href=\"https://bsky.app/profile/rec98.nmlgc.net/post/3lbe4ddeqa222\"\u003emuch more favorably\u003c/a\u003e than \u003ca href=\"https://twitter.com/ReC98Project/status/1859091230224736273\"\u003eTwitter has been doing lately\u003c/a\u003e, despite me having less than \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e10\u003c/sub\u003e of mostly automatically migrated followers there. For now, I'm going to cross-post new stuff to both platforms, but I might eventually spend a push to migrate my entire tweet history over to a self-hosted PDS to own the primary source of this data.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Staying with main menus, but jumping forward to TH04 and TH05 and finalizing some code there. Should be a quick one.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-12-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-10-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-11-22T04:00:03Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-10-22",
      "url": "https://rec98.nmlgc.net/blog/2024-10-22",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-11-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-07-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-10-22\"\u003e\u003ctime datetime=\"2024-10-22T05:18:43Z\"\u003e2024-10-22 05:18\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0286\"\u003eP0286\u003c/a\u003e\n\t\t\ttupblocks (\u003ccode\u003eimport std;\u003c/code\u003e support)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/tupblocks/compare/acb2572...fae347c\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0287\"\u003eP0287\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Code cleanup + Game logic portability, part 2/? + Fixes for bugs and landmines)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0275...47f365a\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0288\"\u003eP0288\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Getting pbg's code through static analysis)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/47f365a...fedaf5e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0289\"\u003eP0289\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Game logic portability, part 3/? + Graphics refactoring, part 3/5: Preparations and colors)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/fedaf5e...82b1b2c\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0290\"\u003eP0290\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Graphics refactoring, part 4/5: Geometry, enumeration, and software rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/82b1b2c...7d64c62\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0291\"\u003eP0291\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Graphics refactoring, part 5/5: Clipping, sprites, and initialization)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/7d64c62...d16c023\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0292\"\u003eP0292\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Cross-platform APIs, part 3/?: Main loop + Main menu refactoring)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/d16c023...fe01bd0\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0293\"\u003eP0293\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Cross-platform APIs, part 4/?: SDL_Renderer backend)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/fe01bd0...2d6ae51\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0294\"\u003eP0294\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Window and scaling modes, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/2d6ae51...37cc78b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0295\"\u003eP0295\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Window and scaling modes, part 2/2 + Hotkeys) + Website (Adding missing money amounts to the log)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/37cc78b...P0275\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#trials-2024-10-22 thead tr th:not(:last-child),\n\t#trials-2024-10-22 thead tr td:nth-child(5),\n\t#trials-2024-10-22 tbody tr:nth-child(3n + 1) td:nth-child(7),\n\t#trials-2024-10-22 tbody tr:not(:nth-child(3n + 1)) td:nth-child(6) {\n\t\tborder-right: var(--table-border-thick);\n\t}\n\n\t#trials-2024-10-22 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\n\t#trials-2024-10-22 tbody td {\n\t\ttext-align: right;\n\t}\n\n\t#trials-2024-10-22 tbody tr .build {\n\t\tborder-right: var(--table-border-thick);\n\t\tfont-family: unset;\n\t}\n\n\t.perf-2024-10-22 {\n\t\tfont-size: 82.5%;\n\t}\n\n\t.perf-2024-10-22 thead th:nth-child(1) {\n\t\ttext-align: right;\n\t}\n\t.perf-2024-10-22 thead th:nth-child(2),\n\t.perf-2024-10-22 tbody td {\n\t\ttext-align: left;\n\t\twidth: 1024px;\n\t}\n\n\t.perf-2024-10-22 tbody th {\n\t\tfont-weight: normal;\n\t}\n\n\t.perf-2024-10-22 tbody tr:not(:last-child) {\n\t\tborder-bottom: var(--table-border);\n\t}\n\n\t.perf-2024-10-22 tbody tr div {\n\t\twidth: calc(100% - 11.25ch);\n\t\twhite-space: nowrap;\n\t\tjustify-self: left;\n\t}\n\n\t.perf-2024-10-22 tbody tr div .perfbar {\n\t\tborder-right: 1px solid white;\n\t}\n\n\t.perf-2024-10-22 tbody tr div span:last-child {\n\t\tpadding-left: 0.25em;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tAnd then, the Shuusou Gyoku renderer rewrite escalated to another 10-push monster that delayed the planned Seihou Summer™ straight into mid-fall. Guess that's just how things go these days at my current level of quality. Testing and polish made up half of the development time of this new build, which probably doesn't surprise anyone who has ever dealt with GPUs and drivers…\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#cleanup-2024-10-22\"\u003eCodebase cleanup and portability\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#modules-2024-10-22\"\u003eRolling out C++ Standard Library Modules\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#issues-2024-10-22\"\u003ePearls uncovered by static analysis\u003c/a\u003e (← start here if you don't particularly care about C++)\u003c/li\u003e\u003cli\u003e\u003ca href=\"#arch-2024-10-22\"\u003eFinishing the new graphics architecture (and discovering every remaining 8-bit/16-bit inconsistency)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl-2024-10-22\"\u003eStarting the SDL port with the simple things\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#benchmark-2024-10-22\"\u003eBenchmarking SDL_Renderer's driver APIs\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#lens-2024-10-22\"\u003ePorting the lens ball effect\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#window-2024-10-22\"\u003eAdding scaled window and borderless fullscreen modes\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#hotkeys-2024-10-22\"\u003eAdding hotkeys for the above\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#scalemodes-2024-10-22\"\u003eScreenshots, and how they force us to rethink scaling\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#subpixels-2024-10-22\"\u003eAchieving subpixel accuracy\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#subpixels-2024-10-22\"\u003eDisplaced triangles\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#lines-2024-10-22\"\u003eLine rendering\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#future-2024-10-22\"\u003eFuture work\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#kog-2024-10-22\"\u003eKioh Gyoku hype 🙌\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#palettized-2024-10-22\"\u003ePreserving the palettized 8-bit render path\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl3-2024-10-22\"\u003eUpdating to SDL 3\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#echo-2024-10-22\"\u003eRecreating the Sound Canvas VA BGM packs (now with panning delay)\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"cleanup-2024-10-22\"\u003e\u003cp\u003e\n\tBut first, let's finally deploy \u003ca href=\"https://wg21.link/P2465R3\"\u003eC++23 Standard Library Modules\u003c/a\u003e! I've been waiting for the promised compile-time improvements of modules for 4 years now, so I was bound to jump at the very first possible opportunity to use them in a project. Unfortunately, MSVC further complicates such a migration by adding one particularly annoying proprietary requirement:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eOur own code wants to use both static analysis and modules.\u003c/li\u003e\n\t\u003cli\u003eMSVC therefore insists that the modules are also compiled with static analysis enabled.\u003c/li\u003e\n\t\u003cli\u003eBut this in turn forces every other translation unit that consumes these modules, including pbg's code, to be built with static analysis enabled as well, …\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t… which means we're now faced with hundreds of little warnings and C++ Core Guideline violations from pbg's code. Sure, we could just disable all warnings when compiling pbg's source files and get on with rolling out modules, because they would still count as \"statically analyzed\" in this case. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e But that's silly. As development continues and we write more of our own modern code, more and more of it will invariably end up within pbg's files, merging and intertwining with original game code. Therefore, not analyzing these files is bound to leave more and more potential issues undetected. Heck, I've \u003ci\u003ealready\u003c/i\u003e committed a \u003ca href=\"https://isocpp.org/wiki/faq/ctors#static-init-order\"\u003estatic initialization order fiasco\u003c/a\u003e by accident that only turned into an actual crash halfway through the development of these 10 pushes. Static analysis would have caught that issue.\u003cbr\u003e\n\tSo let's meet in the middle. Focus on a sensible subset of warnings that we would appreciate in our own code or that could reveal bugs or portability issues in pbg's code, but disable anything that would lead to giant and dangerous refactors or that won't apply to our own code. For example, it would sure be nice to rewrite certain instances of \u003ccode\u003egoto\u003c/code\u003e spaghetti into something more structured, but since we \u003ci\u003eourselves\u003c/i\u003e won't use \u003ccode\u003egoto\u003c/code\u003e, it's not worth worrying about within a porting project.\n\u003c/p\u003e\u003cp\u003e\n\tAfter deduplicating lots of code to reduce the sheer \u003ci\u003enumber\u003c/i\u003e of warnings, the single biggest remaining group of issues were the C-style casts littered throughout the code. These combine the unconstrained unsafety of C with the fact that most of them use the classic uppercase integer types from \u003ccode\u003e\u0026lt;windows.h\u0026gt;\u003c/code\u003e, adding a further portability aspect to this class of issues.\u003cbr\u003e\n\tThe perhaps biggest problem about them, however, is that casts are a \u003ci\u003eunary operator\u003c/i\u003e with its own place in the precedence hierarchy. If you don't surround them with even more brackets to indicate the exact order of operations, you can confuse and mislead the hell out of anyone trying to read your code. This is how we end up with the single most devious piece of arithmetic I've found in this game so far:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003eBYTE d = (BYTE)(t-\u003ed+4)/8;\t// 修正 8 ごとで 32 分割だからズラシは 4\u003c/pre\u003e\u003cfigcaption\u003e\n\t\t\u003ccode\u003et-\u003ed\u003c/code\u003e is a \u003ccode\u003eBYTE\u003c/code\u003e as well.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIf you don't look at vintage C code all day, this cast looks redundant at first glance. Why would you separately cast the result of this expression to the type of the receiving variable? However, casting has higher precedence than division, so the code actually downcasts the \u003ci\u003edividend\u003c/i\u003e, \u003ccode\u003e(t-\u003ed+4)\u003c/code\u003e, \u003ci\u003enot\u003c/i\u003e the result of the division. And why would pbg do \u003ci\u003ethat\u003c/i\u003e? Because the regular, untyped \u003ccode\u003e4\u003c/code\u003e is implicitly an \u003ccode\u003eint\u003c/code\u003e, C promotes \u003ccode\u003et-\u003ed\u003c/code\u003e to \u003ccode\u003eint\u003c/code\u003e as well, thus avoiding the intended 8-bit overflow. If \u003ccode\u003et-\u003ed\u003c/code\u003e is 252, removing the cast would therefore result in \u003ccode\u003e\n\t\t((int{\u0026nbsp;252\u0026nbsp;}\u0026nbsp;+\u0026nbsp;int{\u0026nbsp;4\u0026nbsp;})\u0026nbsp;/\u0026nbsp;8)\u0026nbsp;=\n\t\t256\u0026nbsp;/\u0026nbsp;8\u0026nbsp;=\u003c/code\u003e\n\t32, not the 0 we wanted to have. And since this line is part of the \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/GIAN07/TAMA.CPP#L402-L424\"\u003esprite selection for VIVIT-captured-'s feather bullets\u003c/a\u003e, omitting the cast has a visible effect on the game:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 768px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-feathercast-missing.png?c91e2373\" data-title=\"Removed cast\" class=\"active\" width=\"768\" alt=\"A circle of VIVIT-captured-'s feather bullets as shown shortly after the beginning of her final form, but with one feather bullet turned into a \u0026quot;YZ\u0026quot; sprite due to a removed downcast of the dividend in the angle calculation\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-feathercast-correct.png?fddcdc67\" data-title=\"Original game\" width=\"768\" alt=\"A circle of VIVIT-captured-'s feather bullets as shown shortly after the beginning of her final form, rendered correctly\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tThe first file in \u003ccode\u003eGRAPH.DAT\u003c/code\u003e explains what we're seeing here.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo let's add brackets and replace the C-style cast with a C++ \u003ccode\u003estatic_cast\u003c/code\u003e to make this more readable:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003econst auto d = (static_cast\u0026lt;uint8_t\u0026gt;(t-\u003ed + 4) / 8);\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut that only addresses the precedence pitfall and doesn't tell us \u003ci\u003ewhy\u003c/i\u003e we need that cast in the first place. Can we be more explicit?\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003econst auto d = (((t-\u003ed + 4) \u0026 0xFF) / 8);\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat might be better, but still assumes familiarity with integer promotion for that mask to not appear redundant. What's the strongest way we could scream \u003ci\u003einteger promotion\u003c/i\u003e to anyone trying to touch this code?\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003econst auto d = (Cast::down_sign\u0026lt;uint8_t\u0026gt;(t-\u003ed + 4) / 8);\u003c/pre\u003e\n\t\u003cfigcaption\u003eOf course, I also added a lengthy comment above this line.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow we're talking! \u003ccode\u003eCast::down_sign()\u003c/code\u003e uses \u003ccode\u003estatic_assert\u003c/code\u003es to enforce that its argument must be both larger and differently signed than the target type inside the angle brackets. This unmistakably clarifies that we want to truncate a promoted integer addition because the code wouldn't even compile if the argument was already a \u003ccode\u003euint8_t\u003c/code\u003e. As such, \u003ca href=\"https://github.com/nmlgc/ssg/blob/P0295/game/cast.h\"\u003ethis new set of casts I came up with\u003c/a\u003e goes even further in terms of clarifying intent than the \u003ca href=\"https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-narrowing\"\u003e\u003ccode\u003egsl::narrow_cast()\u003c/code\u003e\u003c/a\u003e proposed by the C++ Core Guidelines, which is \u003ca href=\"https://github.com/microsoft/GSL/blob/3275f9ccb93a559a34baeb52f86c192a30e914b0/include/gsl/util#L102-L110\"\u003epurely informational\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tOK, so replacing C-style casts is better for readability, but why care about it during a porting project? Wouldn't it be more efficient to just \u003ccode\u003etypedef\u003c/code\u003e the \u003ccode\u003e\u0026lt;windows.h\u0026gt;\u003c/code\u003e types for the Linux code and be done with it? Well, the ECL and SCL interpreters provide another good reason not to do that:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003ecase(ECL_SETUP): // 敵の初期化\n\te-\u003ehp    = *(DWORD *)(\u0026cmd[1]);\n\te-\u003escore = *(DWORD *)(\u0026cmd[1+4]);\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIn these instances, the \u003ccode\u003eDWORD\u003c/code\u003e type communicates that this codebase originally targeted Windows, and implies that the \u003ccode\u003ecmd\u003c/code\u003e buffer stores these 32-bit values in little-endian format. Therefore, replacing \u003ccode\u003eDWORD\u003c/code\u003e with the seemingly more portable \u003ccode\u003euint32_t\u003c/code\u003e would actually be \u003ci\u003eworse\u003c/i\u003e as it no longer communicates the endianness assumption. Instead, let's make the endianness explicit:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre class=\"chroma\"\u003ecase(ECL_SETUP): // 敵の初期化\n\u003cspan class=\"gi\"\u003e+\te-\u003ehp    = U32LEAt(\u0026cmd[1 + 0]);\u003c/span\u003e\n\u003cspan class=\"gi\"\u003e+\te-\u003escore = U32LEAt(\u0026cmd[1 + 4]);\u003c/span\u003e\u003c/pre\u003e\u003cfigcaption\u003e\n\tNo surprises once we port this game to a big-endian system – and much fewer characters than a pedantic \u003ccode\u003ereinterpret_cast\u003c/code\u003e, too.\n\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"modules-2024-10-22\"\u003e\u003cp\u003e\n\tWith that and \u003ca href=\"https://github.com/nmlgc/tupblocks/compare/acb2572...fae347c\"\u003eanother pile of improvements for my Tup building blocks\u003c/a\u003e, we finally get to deploy \u003ccode\u003eimport std;\u003c/code\u003e across the codebase, and improve our build times by…\u003cbr\u003e\n\t…not exactly the mid-three-digit percentages I was hoping for. Previously, a full parallel compilation of the Debug build took roughly 23.9s on my 6-year-old 6-core Intel Core i5-8400T. With modules, we now need to compile the C++ standard library a single time on every from-scratch rebuild or after a compiler version update, which adds an unparallelizable ~5.8s to the build time. After that though, all C++ code compiles within ~12.4s, yielding a still decent 92% speedup for regular development. 🎉 Let's look more closely into these numbers and the resulting state of the codebase:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eExpecting three-digit speedups was definitely a bit premature as there were still several game-code translation units that \u003ccode\u003e#include \u0026lt;windows.h\u0026gt;\u003c/code\u003e. The subsequent graphics work removed a few more of these instances, which did bring the speedup into the three-digit range with a compilation time of ~11.6s by the end of P0295.\u003c/li\u003e\n\t\u003cli\u003eSupporting \u003ccode\u003eimport\u003c/code\u003e-then-\u003ccode\u003e#include\u003c/code\u003e is crucial for supporting gradual migrations from headers to modules, but this is one of the most challenging features for compilers to implement, with both \u003ca href=\"https://www.reddit.com/r/cpp/comments/1b0zem7/comment/ksc6xws\"\u003eMSVC\u003c/a\u003e and \u003ca href=\"https://github.com/llvm/llvm-project/issues/61465\"\u003eClang\u003c/a\u003e struggling. By now, MSVC admirably seems to handle all of the cases I ran into, \u003ca href=\"https://github.com/nmlgc/ssg/commit/4e57a0e1489a20deaa5df51a023def9c343a0800\"\u003eexcept for this one\u003c/a\u003e:\u003cfigure\u003e\n\t\t\u003cpre\u003e// ENEMY.H\nimport std.compat;\n\ninline bool LaserHITCHK(/* … */)\n{\n\t// […]\n\n\t// Causes the compiler to instantiate the overloaded C++ version of\n\t// std::abs() via the global namespace re-export in `std.compat`,\n\t// not the C version.\n\tw = abs(-sinl(d,tx) + cosl(d,ty));\n\n\t// […]\n}\n\n// Later, in another header file included via \u0026lt;windows.h\u0026gt;…\n// This header defines the C version of abs(), thus causing a duplicate\n// definition error.\n#include \u0026lt;stdlib.h\u0026gt;\u003c/pre\u003e\n\t\u003c/figure\u003eThe best solution here is to simply not define functions in headers. We could also blame this one on the \u003ccode\u003estd.compat\u003c/code\u003e module which re-exports the C standard library into the global namespace and thus creates these duplicated definitions in the first place, but come on, \u003ccode\u003estd::uint32_t\u003c/code\u003e is 13 characters. That is way too much typing and screen space for referring to basic fixed-size integer types.\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/2024-07-09#win32-2024-07-09\"\u003e📝 As we've thoroughly explored last time\u003c/a\u003e, Tup still ain't batching. Could it be that Tup's paradigm of spawning one \u003ccode\u003ecl.exe\u003c/code\u003e process per translation unit prevents us from using modules to their full throughput potential? The \u003ca href=\"https://learn.microsoft.com/en-us/cpp/build/reference/cgthreads-code-generation-threads\"\u003e\u003ccode\u003e/cgthreads1\u003c/code\u003e\u003c/a\u003e flag seems to help in this regard. Let's do some profiling using \u003ccode\u003ecl.exe\u003c/code\u003e's \u003ca href=\"https://www.geoffchappell.com/studies/msvc/cl/cl/options/b$t.htm\"\u003eundocumented \u003ccode\u003e/Bt\u003c/code\u003e flag\u003c/a\u003e to find out how the compilation times are distributed between the parsing and semantic analysis frontend (\u003ccode\u003ec1*.dll\u003c/code\u003e) and the code generation backend (\u003ccode\u003ec2.dll\u003c/code\u003e):\n\t\u003cfigure\u003e\u003ctable id=\"trials-2024-10-22\" class=\"numbers trials\"\u003e\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth colspan=\"2\" rowspan=\"2\"\u003e\u003c/th\u003e\n\t\t\t\u003cth colspan=\"5\"\u003eGame code \u003csmall\u003e(60 TUs around migration, 58 TUs at end of P0295)\u003c/small\u003e\u003c/th\u003e\n\t\t\t\u003cth colspan=\"5\"\u003eLibrary code  \u003csmall\u003e(211 TUs)\u003c/small\u003e\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003ctd\u003eΣ \u003ccode\u003ec1xx.dll\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003eΣ \u003ccode\u003ec2.dll\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003eΣ Total\u003c/td\u003e\n\t\t\t\u003ctd\u003e% Frontend\u003c/td\u003e\n\t\t\t\u003ctd\u003eReal time\u003c/td\u003e\n\t\t\t\u003ctd\u003eΣ \u003ccode\u003ec1.dll\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003eΣ \u003ccode\u003ec2.dll\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003eΣ Total\u003c/td\u003e\n\t\t\t\u003ctd\u003e% Frontend\u003c/td\u003e\n\t\t\t\u003ctd\u003eReal time\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth rowspan=\"3\" style=\"border-bottom: var(--table-border-thick);\"\u003eTup\u003c/th\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/b1852be752a8069ae0d248396ca3a2adcb0c9c79\"\u003eBefore modules\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e127.4s\u003c/td\u003e\n\t\t\t\u003ctd\u003e3.2s\u003c/td\u003e\n\t\t\t\u003ctd\u003e130.6s\u003c/td\u003e\n\t\t\t\u003ctd\u003e97.5%\u003c/td\u003e\n\t\t\t\u003ctd\u003e23.9s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e53.3s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e7.9s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e61.2s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e87.1%\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e14.2s\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/a2eb8fa6dbb86c5ffa3b654bed427be8630dc0bc\"\u003eAfter modules\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e59.1s\u003c/td\u003e\n\t\t\t\u003ctd\u003e3.1s\u003c/td\u003e\n\t\t\t\u003ctd\u003e62.2s\u003c/td\u003e\n\t\t\t\u003ctd\u003e95.1%\u003c/td\u003e\n\t\t\t\u003ctd\u003e12.4s\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr style=\"border-bottom: var(--table-border-thick);\"\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/P0295\"\u003eAt end of P0295\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e55.1s\u003c/td\u003e\n\t\t\t\u003ctd\u003e3.1s\u003c/td\u003e\n\t\t\t\u003ctd\u003e58.2s\u003c/td\u003e\n\t\t\t\u003ctd\u003e94.6%\u003c/td\u003e\n\t\t\t\u003ctd\u003e11.6s\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth rowspan=\"3\"\u003eBatched\u003c/th\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/b1852be752a8069ae0d248396ca3a2adcb0c9c79\"\u003eBefore modules\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e117.8s\u003c/td\u003e\n\t\t\t\u003ctd\u003e1.5s\u003c/td\u003e\n\t\t\t\u003ctd\u003e119.35s\u003c/td\u003e\n\t\t\t\u003ctd\u003e98.7%\u003c/td\u003e\n\t\t\t\u003ctd\u003e21.9s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e42.3s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e2.5s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e44.8s\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e94.5%\u003c/td\u003e\n\t\t\t\u003ctd rowspan=\"3\"\u003e8.9s\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/a2eb8fa6dbb86c5ffa3b654bed427be8630dc0bc\"\u003eAfter modules\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e51.8s\u003c/td\u003e\n\t\t\t\u003ctd\u003e1.3s\u003c/td\u003e\n\t\t\t\u003ctd\u003e53.1s\u003c/td\u003e\n\t\t\t\u003ctd\u003e97.6%\u003c/td\u003e\n\t\t\t\u003ctd\u003e9.7s\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003ctd class=\"build\"\u003e\u003ca href=\"https://github.com/nmlgc/ssg/commit/P0295\"\u003eAt end of P0295\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e48.5s\u003c/td\u003e\n\t\t\t\u003ctd\u003e1.3s\u003c/td\u003e\n\t\t\t\u003ctd\u003e49.8s\u003c/td\u003e\n\t\t\t\u003ctd\u003e97.4%\u003c/td\u003e\n\t\t\t\u003ctd\u003e9.3s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\t\tCumulative frontend and backend compilation times of a Debug build on my system, as reported by \u003ccode\u003e/Bt\u003c/code\u003e, together with the total real time. Since the library code is all C and therefore unaffected by modules, the numbers are the average of the builds at all three tested commits.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\n\tSo yes, the Tup tax is real and adds somewhere between 30 and 40\u0026nbsp;ms per translation unit to the compilation time. \u003ccode\u003ecl.exe\u003c/code\u003e is simply better at parallelizing itself than any attempt to parallelize it from the outside. It feels inevitable that I'll eventually just fork Tup and add this batching functionality myself; the entire trajectory of my development career has been pointing towards that goal, and it would be the logical conclusion of my C++ build frustrations. But certainly not any time soon; the cost is not \u003ci\u003etoo\u003c/i\u003e high all things considered, I update libraries maybe once every second push, and I'll have done enough build system work for the foreseeable future after the Linux port is done.\u003cbr\u003e\n\tThese numbers also explain why \u003ccode\u003e/cgthreads1\u003c/code\u003e has no measurable performance benefit for this codebase. You might \u003ci\u003ethink\u003c/i\u003e it's a good idea because Tup spawns one parallel \u003ccode\u003ecl.exe\u003c/code\u003e process per CPU core and we can't get any more real parallelism in such a situation. However, that's not what this option does – it only limits the number of \u003cb\u003ec\u003c/b\u003eode \u003cb\u003eg\u003c/b\u003eeneration threads, and as the numbers show, code generation is the opposite of our bottleneck.\u003c/li\u003e\n\t\u003cli\u003eHowever, these compile time improvements come at the cost of modules completely breaking any of the major LSPs at this point in time:\u003cul\u003e\n\t\t\u003cli\u003eThe C++ extension for Visual Studio Code crashes with this error in any file that includes several headers in addition to modules:\n\t\t\u003cblockquote\u003eIntelliSense process crash detected: handle_initialize\nQuick info operation failed: FE: 'Compiler exited with error - No IL available'\u003c/blockquote\u003e\n\t\tConsequently, it no longer provides any IntelliSense for either header or standard library code.\u003c/li\u003e\n\t\t\u003cli\u003eThe big Visual Studio IDE politely remarks that \u003ci\u003e\u003cq\u003eC++ IntelliSense support for C++20 Modules is currently experimental\u003c/q\u003e\u003c/i\u003e and then silently doesn't provide IntelliSense for anything either.\u003c/li\u003e\n\t\t\u003cli\u003eWhen given a \u003ccode\u003ecompile_commands.json\u003c/code\u003e from Tup via \u003ccode\u003etup compiledb\u003c/code\u003e, clangd does continue to provide IntelliSense for both header code and the C++ standard library, but its actual lack of module support puts so many false-positive squiggly lines all over the code that it's not worth using either.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut in the end, the halved compile times during regular development are well worth sacrificing IntelliSense for the time being… especially given that I am the only one who has to live in this codebase. 🧠 And besides, modules bring their own set of productivity boosts to further offset this loss: We can now freely use modern C++ standard library features at a minuscule fraction of their usual compile time cost, and get to cut down the number of necessary \u003ccode\u003e#include\u003c/code\u003e directives. Once you've experienced the simplicity of \u003ccode\u003eimport std;\u003c/code\u003e, headers and their associated micro-optimization of \u003ccode\u003e#include\u003c/code\u003e costs immediately feels archaic. Try the equally undocumented \u003ca href=\"https://aras-p.info/blog/2019/01/21/Another-cool-MSVC-flag-d1reportTime/\"\u003e\u003ccode\u003e/d1reportTime\u003c/code\u003e\u003c/a\u003e flag to get an idea of the compile time impact of function definitions and template instantiations inside headers… I've definitely been moving quite a few of those to \u003ccode\u003e.cpp\u003c/code\u003e files within these 10 pushes.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, it still felt like the earliest possible point in time where doing this was feasible at all. Without LSP support, modules still feel way too bleeding-edge for a feature that was added to the C++ standard 4 years ago. This is why I only chose to use them for covering the C++ standard library for now, as we have yet to see how well GCC or Clang handle it all for the Linux port. If we run into any issues, it makes sense to polyfill any workarounds as part of the Tup building blocks instead of bloating the code with all the standard library header inclusions I'm so glad to have gotten rid of.\u003cbr\u003e\n\tWell, \u003ci\u003ealmost all of them\u003c/i\u003e, because we \u003ci\u003estill\u003c/i\u003e have to \u003ccode\u003e#include \u0026lt;assert.h\u0026gt;\u003c/code\u003e and \u003ccode\u003e\u0026lt;stdlib.h\u0026gt;\u003c/code\u003e because modules can't expose preprocessor macros and C++23 has no macro-less alternative for \u003ccode\u003eassert()\u003c/code\u003e and \u003ccode\u003eoffsetof()\u003c/code\u003e. 🤦 \u003ca href=\"https://en.cppreference.com/w/cpp/language/attributes/assume\"\u003e\u003ccode\u003e[[assume()]]\u003c/code\u003e exists\u003c/a\u003e, but it's the exact opposite of \u003ccode\u003eassert()\u003c/code\u003e. How disappointing.\n\u003c/p\u003e\u003chr id=\"issues-2024-10-22\"\u003e\u003cp\u003e\n\tAs expected, static analysis also brought a small number of pbg code pearls into focus. This list would have fit better into the static analysis section, but I figured that my audience might not necessarily care about C++ all that much, so here it is:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eShuusou Gyoku only ever seeds its RNG in three places:\u003cul\u003e\n\t\t\u003cli\u003eAt program startup (with 0),\u003c/li\u003e\n\t\t\u003cli\u003eimmediately before the game picks a random attract replay after 10 seconds of no input in the top level of the menu (with the current system time in milliseconds), and, obviously,\u003c/li\u003e\n\t\t\u003cli\u003ewhen starting a replay (with the replay's recorded seed), which ironically counteracts the above seed immediately after the game selected the replay.\u003c/li\u003e\n\t\u003c/ul\u003e\n\tSince neither the main menu nor any of the three weapon previews utilize the RNG, any new \u003ci\u003eunrecorded\u003c/i\u003e round started immediately after launching the \u003ccode\u003e.exe\u003c/code\u003e will always start with a seed of 0. Similarly, recorded rounds \u003ca href=\"https://github.com/nmlgc/ssg/blob/ba93920997d70e0f47cdb18c61056246cc79a784/GIAN07/DEMOPLAY.CPP#L52-L54\"\u003ecalculate their seed from the next two RNG numbers\u003c/a\u003e, and will always start with a seed of 347 in the same situation. RNG manipulation is therefore as simple as crafting a replay file with the intended seed, starting its playback, and immediately quitting back to the main menu. The stage of the crafted replay only matters insofar as Stage 6 starts out by reading 320 numbers from the RNG to initialize its wavy clock and shooting star animations, so you'd preferably use any other stage as all of them take a while until they read their first random number.\u003cbr\u003e\n\tOf course, even a shmup with a fixed seed is only as deterministic as the input it receives from the player, and typical human input deviations will quickly add more randomness back into the game.\u003c/li\u003e\n\t\u003cli\u003eThe effective cap of stage enemies, player shots, enemy bullets, lasers, and items is 1 entity smaller than their static array sizes would suggest. pbg did this to work around a potential out-of-bounds write in a generic management function.\u003c/li\u003e\n\t\u003cli\u003eThe in-game score display no longer overflows into negative numbers once the score exceeds (2\u003csup\u003e31\u003c/sup\u003e\u0026nbsp;-\u0026nbsp;1) points. Shuusou Gyoku did track the score using a signed 64-bit integer, but pbg accidentally used a 32-bit specifier for \u003ccode\u003esprintf()\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"arch-2024-10-22\"\u003e\u003cp\u003e\n\tAlright, on to graphics! With font rendering and surface management mostly taken care of last year, the main focus for this final stretch was on all the geometric shapes and color gradients. pbg placed a bunch of rather game-specific code in the platform layer directly next to the Direct3D API calls, including point generation for circles and even the colors of \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/DirectXUTYs/DD_GRP3D.CPP#268\"\u003egradient rectangles\u003c/a\u003e, \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/DirectXUTYs/DD_GRP3D.CPP#L294\"\u003egradient polygons\u003c/a\u003e, and \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/DirectXUTYs/DD_GRP3D.CPP#L315-L316\"\u003ethe Music Room's spectrum analyzer\u003c/a\u003e. We don't want to duplicate any of this as part of the new SDL graphics layer, so I moved it all into a new game-level geometry system. By placing both the 8-bit and 16-bit approaches next to each other, this new system also draws more attention to the different approaches used at each bit depth.\u003cbr\u003e\n\tSo far, so boring. Said differences themselves \u003ci\u003eare\u003c/i\u003e rather interesting though, as this refactor uncovered all of the remaining inconsistencies between the two modes:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eIn 8-bit mode, the game draws circles by writing pixels along the accurate outline into the framebuffer. The hardware-accelerated equivalent for the 16-bit mode would be a large unwieldy point list, so the game instead approximates circles by drawing straight lines along a regular 32-sided polygon:\n\t\u003cfigure class=\"pixelated\" style=\"width: 512px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-circles-8-bit.png?9fa06de4\" data-title=\"8-bit\" class=\"active\" width=\"512\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 8-bit mode as shown during the Marisa boss fight\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-circles-16-bit.png?9e561652\" data-title=\"16-bit\" width=\"512\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 16-bit mode as shown during the Marisa boss fight\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\t\tIt's not like the APIs prevent the 16-bit mode from taking the same approach as the 8-bit mode, so I suppose that pbg profiled this and concluded that lines offloaded to the GPU performed better than locking the framebuffer and writing pixels? Then again, given Shuusou Gyoku's comparatively high system requirements…\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cp\u003eFor preservation and consistency reasons, the SDL backend will also go with the approximation, although \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/63\"\u003ewe could provide the accurate rendering of 8-bit mode via point lists if there's interest\u003c/a\u003e.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eThere's an off-by-one error in the playfield clipping region for Direct3D-rendered shapes, which ends at (﻿511,\u0026nbsp;479﻿) instead of (﻿512,\u0026nbsp;480﻿):\n\t\u003cfigure class=\"pixelated\" style=\"width: 640px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-viewport-8-bit.png?7747fb4c\" data-title=\"8-bit\" width=\"640\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 8-bit mode as shown during the Marisa boss fight\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-viewport-16-bit.png?14c4560b\" data-title=\"16-bit\" class=\"active\" width=\"640\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 16-bit mode as shown during the Marisa boss fight\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\t\tThe fix is obvious.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThere's an off-by-one error in the 8-bit rendering code for opaque rectangles that causes them to appear 1 pixel wider than in 16-bit mode. The red backgrounds behind the currently entered score are the only such boxes in the entire game; the transparent rectangles used everywhere else are drawn with the same width in both modes.\n\t\u003cfigure class=\"pixelated\" style=\"width: 912px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-boxes-8-bit.png?eff6d27d\" data-title=\"8-bit\" width=\"912\" class=\"active\" alt=\"Screenshot of Shuusou Gyoku's High Score entry menu in 8-bit mode, highlighting 1st place with red backgrounds that are 1 pixel wider than in 16-bit mode\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-boxes-16-bit.png?f6adff83\" data-title=\"16-bit\" width=\"912\" alt=\"Screenshot of Shuusou Gyoku's High Score entry menu in 16-bit mode, highlighting 1st place with red backgrounds at the correct size passed by game code\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\t\tThe game code also clearly asks for 400 and 14 pixels, respectively.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eIf we move the nice and accurate 8-bit circle outlines closer to the edge of the playfield, we discover, you guessed it, yet another off-by-one error:\n\t\u003cfigure class=\"pixelated\" style=\"width: 640px;\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-circles-8-bit.png?fec226fe\" data-title=\"8-bit\" width=\"640\" class=\"active\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 8-bit mode as shown during the Marisa boss fight, this time with the circles closer to the right edge of the playfield to highlight the off-by-one error in their clipping condition\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-10-22-SH01-OB1-circles-16-bit-fixed.png?bdcfd6f2\" data-title=\"Line approximation (with fixed clipping)\" width=\"640\" alt=\"Screenshot of Shuusou Gyoku's circle drawing in 16-bit mode as shown during the Marisa boss fight and with a fixed playfield clipping region for Direct3D shapes, this time with the circles closer to the right edge of the playfield to compare their correct clipping against the off-by-one error in 8-bit mode\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\t\tNo circle pixels at the right edge of the playfield. Obviously, I had to fix bug #2 in order for the line approximation to not also get clipped at the same coordinate.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThe final off-by-one clipping error can be found in the filled circle part of homing lasers in 8-bit mode, but it's so minor that it doesn't deserve its own screenshot.\u003c/li\u003e\n\t\u003cli\u003eAlso, how about 16-bit circles being off by \u003ci\u003eone full rotation\u003c/i\u003e? \u003ca href=\"https://github.com/nmlgc/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/DirectXUTYs/DD_GRP3D.CPP#L129-L133\"\u003epbg's code originally generated and rendered 720° worth of points\u003c/a\u003e, thus unnecessarily duplicating the number of lines rendered.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tNow that all of the more complex geometry is generated as part of game code, I could simplify most of the engine's graphics layer down to the classic immediate primitives of early 3D rendering: Line strips, \u003ca href=\"https://en.wikipedia.org/wiki/Triangle_strip\"\u003etriangle strips\u003c/a\u003e, and \u003ca href=\"https://en.wikipedia.org/wiki/Triangle_fan\"\u003etriangle fans\u003c/a\u003e, although I'm retaining pbg's dedicated functions for filled boxes and single gradient lines in case a backend can or needs to use special abstractions for these. (Hint, hint…)\n\u003c/p\u003e\u003chr id=\"sdl-2024-10-22\"\u003e\u003cp\u003e\n\tSo, let's add an SDL graphics backend! With all the earlier preparation work, most of the SDL-specific sprite and geometry code turned out as a very thin wrapper around the, for once, truly simple function calls of the DirectMedia layer. Texture loading from the original color-keyed BMP files, for example, turned into a sequence of 7 straight-line function calls, with most of the work done by \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_LoadBMP_RW\"\u003e\u003ccode\u003eSDL_LoadBMP_RW()\u003c/code\u003e\u003c/a\u003e, \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_SetColorKey\"\u003e\u003ccode\u003eSDL_SetColorKey()\u003c/code\u003e\u003c/a\u003e, and \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_CreateTextureFromSurface\"\u003e\u003ccode\u003eSDL_CreateTextureFromSurface()\u003c/code\u003e\u003c/a\u003e. And although \u003ccode\u003eSDL_LoadBMP_RW()\u003c/code\u003e definitely has its fair share of unnecessary allocations and copies, the whole sequence still loads textures ~300\u0026nbsp;µs faster than the old GDI and DirectDraw backend.\n\u003c/p\u003e\u003cp\u003e\n\tBeing more modern than our immediate geometry primitives, \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderGeometry\"\u003eSDL's triangle renderer\u003c/a\u003e only either renders vertex buffers as triangle lists or requires a corresponding index buffer to realize triangle strips and fans. On paper, this would require an additional memory allocation for each rendered shape. But since we know that Shuusou Gyoku never passes more than 66 vertices at once to the backend, we can be fancy and \u003ca href=\"https://github.com/nmlgc/ssg/blob/ba93920997d70e0f47cdb18c61056246cc79a784/platform/sdl/graphics_sdl.cpp#L78-L100\"\u003ecompute two constant index buffers at compile time\u003c/a\u003e. 🧠 \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderGeometryRaw\"\u003e\u003ccode\u003eSDL_RenderGeometryRaw()\u003c/code\u003e\u003c/a\u003e is the true star of the show here: Not only does it allow us to decouple position and color data compared to \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_Vertex\"\u003eSDL's default packed vertex structure\u003c/a\u003e, but it even allows the neat size optimization of 8-bit index buffers instead of enforcing 32-bit ones.\n\u003c/p\u003e\u003cp\u003e\n\tBy far the funniest porting solution can be found in the Music Room's spectrum analyzer, which calls for 144 1-pixel gradient lines of varying heights. SDL_Renderer has no API for rendering lines with multiple colors… which means that we have to render them as 144 quads with a width of 1 pixel. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 960px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-Spectrum-analyzer.png?fc707275\" data-title=\"Solid\" width=\"960\" alt=\"The spectrum analyzer in Shuusou Gyoku's Music Room, at 6× magnification\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-Spectrum-analyzer-SDL-wireframe.png?9aceebc1\" data-title=\"Wireframe\" width=\"960\" class=\"active\" alt=\"The spectrum analyzer in Shuusou Gyoku's Music Room, with a wireframe render of the spectrum overlaid to demonstrate how the new SDL backend renders these gradient lines, at 6× magnification\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tThe wireframe was generated via a raw \u003ccode\u003eglPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\u003c/code\u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut all these simple abstractions have to be implemented somehow, and this is where we get to perhaps the biggest technical advantage of SDL_Renderer over pbg's old graphics backend. We're no longer locked into just a single underlying graphics API like Direct3D 2, but can choose any of the APIs that the team implemented the high-level renderer abstraction for. We can even switch between them at runtime!\u003cbr\u003e\n\tOn Windows, we have the choice between 3 Direct3D versions, 2 OpenGL versions, and the software renderer. And as we're going to see, all we should do here is define a sensible default and then allow players to override it in a dedicated menu:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2024-10-22-SH01-API-menu.png?77e6d57d\" width=\"640\" alt=\"Screenshot of the new API configuration submenu in the P0295 build of Shuusou Gyoku, highlighting the default OpenGL API\"\u003e\n\t\u003cfigcaption\u003eHuh, we default to OpenGL 2.1? Aren't we still on Windows? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSince such a menu is pretty much asking for people to try every GPU ever with every one of these APIs, \u003ca href=\"https://github.com/nmlgc/ssg/issues/65\"\u003ethere are bound to be bugs with certain combinations\u003c/a\u003e. To prevent the potentially infinite workload, these bugs are exempt from \u003ca href=\"/faq#mod-bugs\"\u003emy usual free bugfix policy\u003c/a\u003e as long as we can get the game working on at least one API without issues. The new initialization code should be resilient enough to automatically fall back on one of SDL's other driver APIs in case the default OpenGL 2.1 fails to initialize for whatever reason, and we can still fight about the best default API.\n\u003c/p\u003e\u003cp id=\"benchmark-2024-10-22\"\u003e\u003cp\u003e\n\tBut let's assume the hopefully usual case of a functional GPU with at least decently written drivers where most of the APIs will work without visible issues. Which of them is the most performant/power-saving one on any given system? With every API having a slightly different idea about 3D rendering, there are bound to be \u003ci\u003esome\u003c/i\u003e performance differences, and maybe these even differ between GPUs. But just how large would they be?\u003cbr\u003e\n\tThe answer is yes:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher class=\"inverted\"\u003e\n\t\u003ctable data-title=\"Stage 6, Laser\" class=\"numbers active perf-2024-10-22\"\u003e\n\t\t\u003cthead\u003e\u003ctr\u003e\u003cth\u003eSystem\u003c/th\u003e\u003cth\u003eFPS (lowest | median) / API\u003c/th\u003e\u003c/tr\u003e\u003c/thead\u003e\n\t\t\u003ctbody\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i5-2520M\u003c/strong\u003e (2011)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eIntel HD Graphics 3000\u003c/strong\u003e (2011)\u003cbr\u003e\n\t\t\t\t1120×840\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#efee1d, #faf950 20%, #b9b816 45%, #b9b816 55%, #efee1d); width: max(22.91666665894737%, 2ch);\"\u003e66\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#efee1d, #faf950 20%, #b9b816 45%, #b9b816 55%, #efee1d); width: 43.055555541052634%;\"\u003e190\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(52.083333333333336%, 2ch);\"\u003e150\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 47.916666666666664%;\"\u003e288\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f67728, #ff9e61 20%, #ca5912 45%, #ca5912 55%, #f67728); width: max(1.0416666620689654%, 2ch);\"\u003e3\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f67728, #ff9e61 20%, #ca5912 45%, #ca5912 55%, #f67728); width: 9.027777737931034%;\"\u003e29\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i5-8400T\u003c/strong\u003e (2018)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eIntel UHD Graphics 630\u003c/strong\u003e (2018)\u003cbr\u003e\n\t\t\t\t1280×960\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#b8e03f, #d0ef73 20%, #93b726 45%, #93b726 55%, #b8e03f); width: max(38.13708257958763%, 2ch);\"\u003e217\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#b8e03f, #d0ef73 20%, #93b726 45%, #93b726 55%, #b8e03f); width: 47.100175720412366%;\"\u003e485\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e227, #fef05f 20%, #c8b713 45%, #c8b713 55%, #f5e227); width: max(18.277680145015104%, 2ch);\"\u003e104\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e227, #fef05f 20%, #c8b713 45%, #c8b713 55%, #f5e227); width: 39.8945518549849%;\"\u003e331\u003c/span\u003e\u003cspan\u003eDirect3D 11\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f69d2a, #ffbb63 20%, #cb7a12 45%, #cb7a12 55%, #f69d2a); width: max(10.369068544055944%, 2ch);\"\u003e59\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f69d2a, #ffbb63 20%, #cb7a12 45%, #cb7a12 55%, #f69d2a); width: 14.762741655944057%;\"\u003e143\u003c/span\u003e\u003cspan\u003eDirect3D 12\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(76.97715289982425%, 2ch);\"\u003e438\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 23.022847100175753%;\"\u003e569\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f66934, #ff9770 20%, #d24813 45%, #d24813 55%, #f66934); width: max(0.8787346296296297%, 2ch);\"\u003e5\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f66934, #ff9770 20%, #d24813 45%, #d24813 55%, #f66934); width: 3.8664323703703705%;\"\u003e27\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i7-7700HQ\u003c/strong\u003e (2017)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eNVIDIA GeForce GTX 1070 Max Q\u003c/strong\u003e, thermal-throttled (2017)\u003cbr\u003e\n\t\t\t\t1280×960\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f3e721, #fdf457 20%, #c1b714 45%, #c1b714 55%, #f3e721); width: max(19.54775476988764%, 2ch);\"\u003e141\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f3e721, #fdf457 20%, #c1b714 45%, #c1b714 55%, #f3e721); width: 42.145513830112364%;\"\u003e445\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f0ec1e, #fbf851 20%, #bbb715 45%, #bbb715 55%, #f0ec1e); width: max(21.489854510683763%, 2ch);\"\u003e155\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f0ec1e, #fbf851 20%, #bbb715 45%, #bbb715 55%, #f0ec1e); width: 43.39564168931624%;\"\u003e468\u003c/span\u003e\u003cspan\u003eDirect3D 11\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#ddeb26, #ecf759 20%, #adb81a 45%, #adb81a 55%, #ddeb26); width: max(30.227673109615385%, 2ch);\"\u003e218\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#ddeb26, #ecf759 20%, #adb81a 45%, #adb81a 55%, #ddeb26); width: 41.875033390384615%;\"\u003e520\u003c/span\u003e\u003cspan\u003eDirect3D 12\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#a3da51, #c2ea86 20%, #80b72f 45%, #80b72f 55%, #a3da51); width: max(73.28244270967741%, 2ch);\"\u003e528\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#a3da51, #c2ea86 20%, #80b72f 45%, #80b72f 55%, #a3da51); width: 21.374045790322583%;\"\u003e682\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(90.42995839112345%, 2ch);\"\u003e652\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 9.57004160887655%;\"\u003e721\u003c/span\u003e\u003cspan\u003eOpenGL ES 2.0\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f66d31, #ff996c 20%, #d04d13 45%, #d04d13 55%, #f66d31); width: max(0.5551700181818182%, 2ch);\"\u003e4\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f66d31, #ff996c 20%, #d04d13 45%, #d04d13 55%, #f66d31); width: 5.551700181818182%;\"\u003e44\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel i7-4790\u003c/strong\u003e (2017)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eNVIDIA GeForce GTX 1660 SUPER\u003c/strong\u003e (2019)\u003cbr\u003e\n\t\t\t\t640×480, scaled to 1760×1320\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(92.26519337016575%, 2ch);\"\u003e2004\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 7.734806629834253%;\"\u003e2172\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e128, #ffef61 20%, #c9b713 45%, #c9b713 55%, #f5e128); width: max(52.57826882946357%, 2ch);\"\u003e1142\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e128, #ffef61 20%, #c9b713 45%, #c9b713 55%, #f5e128); width: 4.926335170536433%;\"\u003e1249\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eAMD Ryzen 5800X\u003c/strong\u003e (2020)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eRadeon RX 7900XTX\u003c/strong\u003e (2022)\u003cbr\u003e\n\t\t\t\t2560x1920\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d935, #ffea72 20%, #d3b613 45%, #d3b613 55%, #f6d935); width: max(23.713084343724365%, 2ch);\"\u003e555\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d935, #ffea72 20%, #d3b613 45%, #d3b613 55%, #f6d935); width: 28.37024865627564%;\"\u003e1219\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(52.083333333333336%, 2ch);\"\u003e750\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 47.916666666666664%;\"\u003e1440\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/tbody\u003e\u003c/table\u003e\n\t\u003ctable data-title=\"Extra Stage, Homing\" class=\"numbers perf-2024-10-22\"\u003e\n\t\t\u003cthead\u003e\u003ctr\u003e\u003cth\u003eSystem\u003c/th\u003e\u003cth\u003eFPS (lowest | median) / API\u003c/th\u003e\u003c/tr\u003e\u003c/thead\u003e\n\t\t\u003ctbody\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i5-2520M\u003c/strong\u003e (2011)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eIntel HD Graphics 3000\u003c/strong\u003e (2011)\u003cbr\u003e\n\t\t\t\t1120×840\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d838, #ffe975 20%, #d5b614 45%, #d5b614 55%, #f6d838); width: max(18.749999986666666%, 2ch);\"\u003e33\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d838, #ffe975 20%, #d5b614 45%, #d5b614 55%, #f6d838); width: 32.38636361333334%;\"\u003e90\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(35.79545454545455%, 2ch);\"\u003e63\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 64.20454545454545%;\"\u003e176\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6a829, #ffc362 20%, #ca8412 45%, #ca8412 55%, #f6a829); width: max(2.272727275471698%, 2ch);\"\u003e4\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6a829, #ffc362 20%, #ca8412 45%, #ca8412 55%, #f6a829); width: 27.840909124528302%;\"\u003e53\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i5-8400T\u003c/strong\u003e (2018)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eIntel UHD Graphics 630\u003c/strong\u003e (2018)\u003cbr\u003e\n\t\t\t\t1280×960\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#d4e82c, #e4f55f 20%, #a7b81c 45%, #a7b81c 55%, #d4e82c); width: max(26.406468822427442%, 2ch);\"\u003e133\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#d4e82c, #e4f55f 20%, #a7b81c 45%, #a7b81c 55%, #d4e82c); width: 48.84204007757256%;\"\u003e379\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f68329, #ffa761 20%, #ca6312 45%, #ca6312 55%, #f68329); width: max(11.530815102777778%, 2ch);\"\u003e58\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f68329, #ffa761 20%, #ca6312 45%, #ca6312 55%, #f68329); width: 2.7833001972222213%;\"\u003e72\u003c/span\u003e\u003cspan\u003eDirect3D 11\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6932a, #ffb363 20%, #cb7112 45%, #cb7112 55%, #f6932a); width: max(6.560636185714285%, 2ch);\"\u003e33\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6932a, #ffb363 20%, #cb7112 45%, #cb7112 55%, #f6932a); width: 14.314115314285713%;\"\u003e105\u003c/span\u003e\u003cspan\u003eDirect3D 12\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(72.96222664015905%, 2ch);\"\u003e367\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 27.037773359840955%;\"\u003e503\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f68829, #ffab62 20%, #ca6812 45%, #ca6812 55%, #f68829); width: max(1.391650096385542%, 2ch);\"\u003e7\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f68829, #ffab62 20%, #ca6812 45%, #ca6812 55%, #f68829); width: 15.109343903614457%;\"\u003e83\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel Core i7-7700HQ\u003c/strong\u003e (2017)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eNVIDIA GeForce GTX 1070 Max Q\u003c/strong\u003e, thermal-throttled (2017)\u003cbr\u003e\n\t\t\t\t1280×960\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6a729, #ffc362 20%, #ca8412 45%, #ca8412 55%, #f6a729); width: max(2.8169014074257426%, 2ch);\"\u003e19\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6a729, #ffc362 20%, #ca8412 45%, #ca8412 55%, #f6a729); width: 27.131208292574257%;\"\u003e202\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d437, #ffe674 20%, #d4b213 45%, #d4b213 55%, #f6d437); width: max(19.866567817575756%, 2ch);\"\u003e134\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d437, #ffe674 20%, #d4b213 45%, #d4b213 55%, #f6d437); width: 29.058561882424243%;\"\u003e330\u003c/span\u003e\u003cspan\u003eDirect3D 11\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e128, #ffef60 20%, #c8b713 45%, #c8b713 55%, #f5e128); width: max(32.32023723641026%, 2ch);\"\u003e218\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f5e128, #ffef60 20%, #c8b713 45%, #c8b713 55%, #f5e128); width: 25.50037066358974%;\"\u003e390\u003c/span\u003e\u003cspan\u003eDirect3D 12\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#a9dc4c, #c6eb80 20%, #86b72c 45%, #86b72c 55%, #a9dc4c); width: max(75.61156415533979%, 2ch);\"\u003e510\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#a9dc4c, #c6eb80 20%, #86b72c 45%, #86b72c 55%, #a9dc4c); width: 16.011860644660203%;\"\u003e618\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(50.22222222222222%, 2ch);\"\u003e339\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 49.77777777777778%;\"\u003e675\u003c/span\u003e\u003cspan\u003eOpenGL ES 2.0\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f67e28, #ffa361 20%, #ca5f12 45%, #ca5f12 55%, #f67e28); width: max(1.1860637523809523%, 2ch);\"\u003e8\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f67e28, #ffa361 20%, #ca5f12 45%, #ca5f12 55%, #f67e28); width: 11.267605647619048%;\"\u003e84\u003c/span\u003e\u003cspan\u003eSoftware\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eIntel i7-4790\u003c/strong\u003e (2017)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eNVIDIA GeForce GTX 1660 SUPER\u003c/strong\u003e (2019)\u003cbr\u003e\n\t\t\t\t640×480, scaled to 1760×1320\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(82.2110552763819%, 2ch);\"\u003e1636\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 17.7889447236181%;\"\u003e1990\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6da34, #ffea70 20%, #d2b613 45%, #d2b613 55%, #f6da34); width: max(33.91959797277937%, 2ch);\"\u003e675\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6da34, #ffea70 20%, #d2b613 45%, #d2b613 55%, #f6da34); width: 18.69346732722063%;\"\u003e1047\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\n\t\t\t\t\u003cstrong\u003eAMD Ryzen 5800X\u003c/strong\u003e (2020)\u003cbr\u003e\n\t\t\t\t\u003cstrong\u003eRadeon RX 7900XTX\u003c/strong\u003e (2022)\u003cbr\u003e\n\t\t\t\t2560x1920\n\t\t\t\u003c/th\u003e\u003ctd\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d233, #ffe46f 20%, #d2ae13 45%, #d2ae13 55%, #f6d233); width: max(27.271674820533878%, 2ch);\"\u003e276\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#f6d233, #ffe46f 20%, #d2ae13 45%, #d2ae13 55%, #f6d233); width: 20.84899777946612%;\"\u003e487\u003c/span\u003e\u003cspan\u003eDirect3D 9\u003c/span\u003e\u003c/div\u003e\n\t\t\t\u003cdiv\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: max(67.35905044510386%, 2ch);\"\u003e681\u003c/span\u003e\u003cspan\n\tclass=\"perfbar\" style=\"background: linear-gradient(#98d65b, #bce890 20%, #75b635 45%, #75b635 55%, #98d65b); width: 32.64094955489614%;\"\u003e1011\u003c/span\u003e\u003cspan\u003eOpenGL 2.1\u003c/span\u003e\u003c/div\u003e\n\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/tbody\u003e\u003c/table\u003e\n\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\tComputed using \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/GIAN07/GIAN.CPP#L157-L163\"\u003epbg's original per-second debugging algorithm\u003c/a\u003e. Except for the Intel i7-4790 test, all of these use SDL's default geometry scaling mode as explained further below. The GeForce GTX 1070 could probably be twice as fast if it weren't inside a laptop that thermal-throttles after about 10 seconds of unlimited rendering.\u003cbr\u003e\n\tThe two tested replays decently represent the entire game: In Stage 6, the software renderer frequently drops into low 1-digit FPS numbers as it struggles with the blending effects used by the Laser shot type's bomb, whereas GPUs enjoy the absence of background tiles. In the Extra Stage, it's the other way round: The tiled background \u003ca href=\"https://github.com/nmlgc/ssg/issues/35\"\u003eand a certain large bullet cancel\u003c/a\u003e emphasize the inefficiency of unbatched rendering on GPUs, but the software renderer has a comparatively much easier time.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that's why I picked OpenGL as the default. It's either \u003ci\u003ethe\u003c/i\u003e best or close to the best choice everywhere, and in the one case where it isn't, it doesn't matter because the GPU is powerful enough for the game anyway.\n\u003c/p\u003e\u003cp\u003e\n\tIf those numbers still look way too low for what Shuusou Gyoku is (because they kind of do), you can try enabling SDL's draw call batching by setting the environment variable \u003ccode\u003eSDL_RENDER_BATCHING\u003c/code\u003e to \u003ccode\u003e1\u003c/code\u003e. This at least doubles the FPS for all hardware-accelerated APIs on the Intel UHD\u0026nbsp;630 in the Extra Stage, and astonishingly turns Direct3D\u0026nbsp;11 from the slowest API into by far the fastest one, speeding it up by 22× for a median FPS of \u003ci\u003e1617\u003c/i\u003e. I only didn't activate batching by default because it causes stability issues with OpenGL ES 2.0 on the same system. But honestly, if even a mid-range laptop from 13 years ago manages a stable 60\u0026nbsp;FPS on the default OpenGL driver \u003ci\u003ewhile still scaling the game\u003c/i\u003e, there's no real need to spend budget on performance improvements.\u003cbr\u003e\n\tIf anything, these numbers justify my choice of not focusing on a specific one of these APIs when coding retro games. There are only very few fields that target a wider range of systems with their software than retrogaming, and as we've seen, each of SDL's supported APIs could be the optimal choice on \u003ci\u003esome\u003c/i\u003e system out there.\n\u003c/p\u003e\u003cp\u003e\n\tThe replays we used for testing: \u003ca class=\"download\" href=\"/blog/static/2024-10-22-SH01-Stage-6-and-Extra-replays.zip?383cbf18\" data-kb=\"12.2\"\u003e2024-10-22-SH01-Stage-6-and-Extra-replays.zip \u003c/a\u003e\n\u003c/p\u003e\u003chr id=\"lens-2024-10-22\"\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2023-08-01#sdl-2023-08-01\"\u003e📝 Last year\u003c/a\u003e, it seemed as if the \u003cspan lang=\"ja\" style=\"word-break: keep-all;\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e logo screen's lens ball effect would be one of the more tricky things to port to SDL_Renderer, and that impression was definitely accurate.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-10-22-SH01-Logo.webp?1ae5bcb7\" preload=\"none\" controls data-title=\"Animation\" loop data-active width=\"640\" height=\"200\" data-fps=\"60\" data-frame-count=\"256\" style=\"aspect-ratio: 640 / 200\" data-lossless=\"/blog/static/video/zmbv/2024-10-22-SH01-Logo.avi?eb85fe07\"\u003e\u003csource src=\"/blog/static/video/av1/2024-10-22-SH01-Logo.webm?39fad740\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-10-22-SH01-Logo.webm?3f9eff2c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-10-22-SH01-Logo.webm?fca6cb50\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's 西方Ｐｒｏｊｅｃｔ opening animation. \u003ca href=\"/blog/static/video/zmbv/2024-10-22-SH01-Logo.avi?eb85fe07\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Lens effect starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"192\" data-title=\"Lens effect done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-10-22-SH01-Logo-lens.webp?09a5ce6d\" preload=\"none\" controls data-title=\"Lens region\" loop width=\"640\" height=\"200\" data-fps=\"60\" data-frame-count=\"256\" style=\"aspect-ratio: 640 / 200\" data-lossless=\"/blog/static/video/zmbv/2024-10-22-SH01-Logo-lens.avi?a0519f01\"\u003e\u003csource src=\"/blog/static/video/av1/2024-10-22-SH01-Logo-lens.webm?eb24a51c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-10-22-SH01-Logo-lens.webm?f08cc43a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-10-22-SH01-Logo-lens.webm?acda9653\" type=\"video/webm\"\u003eVideo tracking the 140×140-pixel lens region during Shuusou Gyoku's 西方Ｐｒｏｊｅｃｔ opening animation. \u003ca href=\"/blog/static/video/zmbv/2024-10-22-SH01-Logo-lens.avi?a0519f01\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Lens effect starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"192\" data-title=\"Lens effect done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe effect works by capturing the original 140×140 pixels under the moving lens ball from the framebuffer into a temporary buffer and then overwriting the framebuffer pixels by shifting and stretching the captured ones according to a pre-calculated table. With DirectDraw, this is no big deal because you can simply lock the framebuffer for read and write access. If it weren't for the fact that you need to either generate or hand-write different code for every support bit depth, this would be one of the most natural effects you could implement with such an API. Modern graphics APIs, however, don't offer this luxury because it didn't take long for this feature to become a liability. Even 20 years ago, you'd rather write this sort of effect as a pixel shader that would directly run on the GPU in a much more accelerated way. Which is a non-starter for us – we sure ain't breaking SDL's abstractions to write a separate shader for every one of SDL_Renderer's supported APIs just for a single effect in the logo screen.\u003cbr\u003e\n\tAs such, SDL_Renderer doesn't even begin to provide framebuffer locking. We can only get close by splitting the two operations:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eReading can only be done via \u003ccode\u003e\u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderReadPixels\"\u003eSDL_RenderReadPixels()\u003c/a\u003e\u003c/code\u003e, which is a one-time \u003ccode\u003ememcpy()\u003c/code\u003e from GPU to main memory. Doing that is \u003ca href=\"https://narkive.com/lPJfIGhu.6\"\u003esaid to be extremely slow\u003c/a\u003e, and we'd have to do it once per frame.\u003c/li\u003e\n\t\u003cli\u003eWriting can only be done by getting the new pixels onto a texture first. Which in turn can either be done by \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_UpdateTexture\"\u003eupdating a rectangular area with prepared pixel data from system memory\u003c/a\u003e, or \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_LockTexture\"\u003elocking a rectangular area and writing the pixels into a buffer\u003c/a\u003e. However, even \u003ccode\u003eSDL_LockTexture()\u003c/code\u003e is explicitly labeled as write-only. By returning an effectively uninitialized texture, you're forced to software-render your entire scene onto this texture \u003ci\u003eanyway\u003c/i\u003e after locking.\u003cbr\u003e\n\tThis little detail in the API contract makes locking entirely unusable for this lens effect. Its code does not write to \u003ci\u003eevery\u003c/i\u003e pixel within the 140×140 area and relies on the unwritten pixels retaining their rendered color, just as you would expect regular memory to behave. If we are forced to prepare the full 140×140 pixels on the CPU, we might as well just go for the simpler \u003ca href=\"https://forums.libsdl.org/viewtopic.php?p=40565#40565\"\u003eand faster\u003c/a\u003e \u003ccode\u003eSDL_UpdateTexture()\u003c/code\u003e.\u003cul\u003e\n\t\t\u003cli\u003eAlso, if SDL says \u003ci\u003e\"write-only access\"\u003c/i\u003e, does this mean we can't even be sure that the locked buffer is readable \u003ci\u003eafter\u003c/i\u003e we wrote some pixels and \u003ci\u003ebefore\u003c/i\u003e we unlock the texture again? We'd only have to look at the PC-98's GRCG for an example of memory-mapped I/O where reading and writing can work fundamentally differently depending on the mode register. The OpenGL driver implements texture locking by allocating a separate buffer in main memory and then uploading this modified buffer to the GPU via \u003ca href=\"https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexSubImage2D.xhtml\"\u003e\u003ccode\u003eglTexSubImage2D()\u003c/code\u003e\u003c/a\u003e upon unlocking, but the docs do leave open the possibility for a driver to return a pointer to GPU memory we can't or shouldn't read from.\u003c/li\u003e\n\t\t\u003cli\u003eIn fact, the \u003ci\u003eonly\u003c/i\u003e sanctioned way of reading pixels back from a texture involves turning the texture into a render target and calling \u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWithin these API limitations, we can now cobble together a first solution:\n\u003c/p\u003e\u003col style=\"list-style: decimal;\"\u003e\n\t\u003cli\u003eRely on \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderTargetSupported\"\u003erender-to-texture being supported\u003c/a\u003e. This is the case for all APIs that are currently implemented for SDL 2's renderer and \u003ca href=\"https://github.com/libsdl-org/SDL/commit/dcd17f547324a143d66d79e3b586577a7da558ed\"\u003eSDL 3 even made support mandatory\u003c/a\u003e, but who knows if we ever get our hands on one of the elusive SDL 2 console ports under NDA and encounter one of them that doesn't support it… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eCreate a 640×480 texture that serves as our editable framebuffer.\u003c/li\u003e\n\t\u003cli\u003eCreate a 140×140 buffer in main memory, serving as the input and output buffer for the effect. We don't need the full 640×480 here because the effect only modifies the pixels below the magnified 140×140 area and doesn't push them further outside.\u003c/li\u003e\n\t\u003cli\u003eRetain the original main-memory 140×140 buffer from the DirectDraw implementation that captures the current frame's pixels under the lens ball before we modify the pixels.\u003c/li\u003e\n\t\u003cli\u003eEach frame, we then\u003col style=\"list-style: lower-alpha;\"\u003e\n\t\t\u003cli\u003erender the scene onto 2),\u003c/li\u003e\n\t\t\u003cli\u003ecapture the magnified area using \u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e, reading from 2) and writing to 3),\u003c/li\u003e\n\t\t\u003cli\u003ecopy 3) to 4) using a regular \u003ccode\u003ememcpy()\u003c/code\u003e,\u003c/li\u003e\n\t\t\u003cli\u003eapply the lens effect by shifting around pixels, reading from 4) and writing to 3),\u003c/li\u003e\n\t\t\u003cli\u003ewrite 3) back to 2), and finally\u003c/li\u003e\n\t\t\u003cli\u003euse 2) as the texture for a quad that scales the texture to the size of the window.\u003c/li\u003e\n\t\u003c/ol\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tCompared to the DirectDraw approach, this adds the technical insecurity of render-to-texture support, one additional texture, one additional fullscreen blit, at least one additional buffer, and two additional copies that comprise a round-trip from GPU to CPU and back. It surely would have worked, but the documentation suggestions and \u003ca href=\"https://discourse.libsdl.org/t/27581\"\u003ehorror stories\u003c/a\u003e surrounding \u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e put me off even trying that approach. Also, it would turn out to clash with an implementation detail we're going to look at later.\u003cbr\u003e\n\tHowever, our \u003cq\u003escene\u003c/q\u003e merely consists of a 320×42 image on top of a black background. If we need the resulting pixels in CPU-accessible memory anyway, there's little point in hardware-rendering such a simple scene to begin with, especially if SDL lets you \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_CreateSoftwareRenderer\"\u003ecreate independent software renderers that support the same draw calls but explicitly write pixels to buffers in regular system memory under your full control\u003c/a\u003e.\u003cbr\u003e\n\tThis simplifies our solution to the following:\n\u003c/p\u003e\u003col style=\"list-style: decimal;\"\u003e\n\t\u003cli\u003eCreate a 640×480 surface in main memory, acting as the target surface for \u003ccode\u003eSDL_CreateSoftwareRenderer()\u003c/code\u003e. But since the potentially hardware-accelerated renderer drivers can't render pixels from such surfaces, we still have to\u003c/li\u003e\n\t\u003cli\u003ecreate an additional 640×480 texture in write-only GPU memory.\u003c/li\u003e\n\t\u003cli\u003eRetain the original main-memory 140×140 buffer from the DirectDraw implementation that captures the current frame's pixels under the lens ball before we modify the pixels.\u003c/li\u003e\n\t\u003cli\u003eEach frame, we then\u003col style=\"list-style: lower-alpha;\"\u003e\n\t\t\u003cli\u003esoftware-render the scene onto 1),\u003c/li\u003e\n\t\t\u003cli\u003ecapture the magnified area using a regular \u003ccode\u003ememcpy()\u003c/code\u003e, reading from 1) and writing to 3),\u003c/li\u003e\n\t\t\u003cli\u003eapply the lens effect by shifting around pixels, reading from 3) and writing to 1),\u003c/li\u003e\n\t\t\u003cli\u003eupload all of 1) onto 2), and finally\u003c/li\u003e\n\t\t\u003cli\u003euse 2) as the texture for a quad that scales the texture to the size of the window.\u003c/li\u003e\n\t\u003c/ol\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThis cuts out the GPU→CPU pixel transfer and replaces the second lens pixel buffer with a software-rendered surface that we can freely manipulate. This seems to require more memory at first, but this memory would actually come in handy for screenshots later on. It also requires the game to enter and leave the new dedicated software rendering mode to ensure that the \u003cspan lang=\"ja\" style=\"word-break: keep-all;\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e image gets loaded as a system-memory \"texture\" instead of a GPU-memory one, but that's just two additional calls in the logo and title loading functions.\u003cbr\u003e\n\tAlso, we would now software-render \u003ci\u003eall\u003c/i\u003e of these 256 frames, including the fades. Since software rendering requires the \u003cspan lang=\"ja\" style=\"word-break: keep-all;\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e image to reside in main memory, it's hard to justify an additional GPU upload just to render the 127 frames surrounding the animation.\u003cbr\u003e\n\u003c/p\u003e\u003cp\u003e\n\tStill, we've only eliminated a single copy, and \u003ccode\u003eSDL_UpdateTexture()\u003c/code\u003e can \u003ca href=\"https://github.com/libsdl-org/SDL/blob/378234437fa2a99224391d5578f46971e190c0b7/src/render/direct3d12/SDL_render_d3d12.c#L1700-L1841\"\u003eand will\u003c/a\u003e do even more under the hood. Suddenly, SDL having its own shader language seems like the lesser evil, doesn't it?\n\tWhen writing it out like this, it sure looks as if hardware rendering adds nothing but overhead here. So how about full-on dropping into software rendering and handling the scaling from 640×480 to the window resolution in software as well? This would allow us to cut out steps 2) and d), leaving 1) as our one and only framebuffer.\u003cbr\u003e\n\tIt sure \u003ci\u003esounds\u003c/i\u003e a lot more efficient. But actually \u003ci\u003etrying\u003c/i\u003e this solution revealed that I had a completely wrong idea of the inefficiencies here:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eWe do want to hardware-render the rest of the game, so we'd need to switch from software to hardware at the end of the logo animation. As it turns out, this switch is a rather expensive operation that would add an awkward ~500\u0026nbsp;ms pause between logo and title screen.\u003c/li\u003e\n\t\u003cli\u003eMost importantly, though: Hardware-accelerating the final scaling step is \u003ci\u003ekind of\u003c/i\u003e important these days. SDL's CPU scaling implementation can get \u003ci\u003ereally\u003c/i\u003e slow if a bilinear filter is involved; on my system, software-scaling 62.5 frames per second by 1.75× to 1120×840 pixels increases CPU usage by ~10%-20% in Release mode, and even drops FPS to 50 in Debug mode.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThis was perhaps the biggest lesson in this sudden 25-year jump from optimizing for a PC-98 and suffering under slow DirectDraw and Direct3D wrappers into the present of GPU rendering. Even though some drivers \u003ci\u003etechnically\u003c/i\u003e don't need these redundant CPU copies, a slight bit of added CPU time is still more than worth it if it means that we get to offload the \u003ci\u003eactually\u003c/i\u003e expensive stuff onto the GPU.\n\u003c/p\u003e\u003chr id=\"window-2024-10-22\"\u003e\u003cp\u003e\n\tBut we all know that 4-digit frame rates aren't the main draw of rendering graphics through SDL. Besides cross-platform compatibility, the most useful aspect for Shuusou Gyoku is how SDL greatly simplifies the addition of the scaled window and borderless fullscreen modes you'd expect for retro pixel graphics on modern displays. Of course, allowing all of these settings to be changed in-engine from inside the \u003ccode\u003eGraphic\u003c/code\u003e options menu is the minimum UX comfort level we would accept here – after all, something like a separate DPI-aware dialog window at startup would be harder to port anyway.\u003cbr\u003e\n\tFor each setting, we can achieve this level of comfort in one of two ways:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eWe could simply shut down SDL's underlying render driver, close the window, and reopen/reinitialize the window and driver, reloading any game graphics as necessary. This is the simplest way: We can just reuse our backend's full initialization code that runs at startup and don't need any code on top. However, it would feel rather janky and cheap.\u003c/li\u003e\n\t\u003cli\u003eOr we could use SDL's various setter functions to only apply the single change to the specific setting… and anything that setting depends on. This would feel really smooth to use, but would require additional code with a couple of branches.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tpbg's code already geared slightly towards 2) with its feature to seamlessly change the bit depth. And with the amount of budget I'm given these days, it should be obvious what I went with. This definitely wasn't trivial and involved lots of state juggling and careful ordering of these procedural, imperative operations, even at the level of \"just\" using high-level SDL API calls for everything. It must have undoubtedly been worse for the SDL developers; after all, every new option for a specific parameter multiplies the amount of potential window state transitions.\u003cbr\u003e\n\tIn the end though, most of it ended up working at our preferred high level of quality, leaving only a few cases where either SDL or the driver API forces us to throw away and recreate the window after all:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhen changing rendering APIs, because certain API transitions would fail to initialize properly and only leave a black window,\u003c/li\u003e\n\t\u003cli\u003ewhen changing into exclusive fullscreen when using Direct3D 9, because that driver only supports exclusive fullscreen on the display the window was spawned on (\u003ca href=\"https://github.com/libsdl-org/SDL/pull/7317#issuecomment-1430809640\"\u003ethis is a known issue, but Direct3D 9 is old and no one has cared enough to investigate it more deeply, and it might very well be an unfixable driver limitation)\u003c/a\u003e, and\u003c/li\u003e\n\t\u003cli\u003ewhen changing from borderless fullscreen into exclusive fullscreen on any API. \u003ca href=\"https://github.com/libsdl-org/SDL/issues/11047\"\u003eThis one is fixed in SDL 3\u003c/a\u003e, and they may or may not backport a fix in response to my bug report.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs for the actual settings, I decided on making the windowed-mode scale factor customizable at intervals of 0.25, or 160×120 pixels, up to the taskbar-excluding resolution of the current display the game window is placed on. Sure, \u003ca href=\"https://tanalin.com/en/articles/integer-scaling/\"\u003erestricting the factor to integer values is the idealistically correct thing to do\u003c/a\u003e, but 640×480 is a rather large source resolution compared to the retro consoles where integer scaling is typically brought up. Hence, such a limitation would be suboptimal for a large number of displays, most notably any old 720p display or those laptop screens with 1366×768 resolutions.\u003cbr\u003e\n\tIn the new borderless fullscreen mode, the configurable scaling factor breaks down into all three possible interpretations of \"fitting the game window onto the whole screen\":\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eA \u003ccode\u003e[Integer]\u003c/code\u003e fit that applies the largest possible integer scaling factor and windowboxes the game accordingly,\u003c/li\u003e\n\t\u003cli\u003ea \u003ccode\u003e[4:3]\u003c/code\u003e fit that stretches the game as large as possible while maintaining the original aspect ratio and either pillarboxes the game on landscape displays or letterboxes it on portrait ones,\u003c/li\u003e\n\t\u003cli\u003eand the cursed, aspect ratio-ignoring \u003ccode\u003e[Stretch]\u003c/code\u003e fit that may or may not improve gameplay for someone out there, but definitely evokes nostalgia for stretching Game Boy (Color) games on a Game Boy Advance.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWhat \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/70\"\u003ecurrently\u003c/a\u003e \u003ci\u003ecan't\u003c/i\u003e be configured is the image filter used for scaling. The game always uses nearest-neighbor at integer scaling factors and bilinear filtering at fractional ones.\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-720p-borderless-integer.png?aa268a25\"\n\t\tdata-title=\"Integer\"\n\t\talt=\"Screenshot of the Integer fit option in the borderless fullscreen mode of the P0295 Shuusou Gyoku build, as captured on a 1280×720 display\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-720p-borderless-aspect.png?5ed7b78a\"\n\t\tdata-title=\"4:3\"\n\t\talt=\"Screenshot of the 4:3 fit option in the borderless fullscreen mode of the P0295 Shuusou Gyoku build, as captured on a 1280×720 display\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-720p-borderless-stretch.png?6883592c\"\n\t\tdata-title=\"Stretch\"\n\t\talt=\"Screenshot of the Stretch fit option in the borderless fullscreen mode of the P0295 Shuusou Gyoku build, as captured on a 1280×720 display\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tThe three scaling options available in borderless fullscreen mode as rendered on a 1280×720 display, which is one of the worst display resolutions you could play this game on.\u003cbr\u003e\n\t\tAnd yes – as the presence of the \u003ccode\u003eFullScr[Borderless]\u003c/code\u003e option implies, the new build also still supports exclusive, display mode-changing 640×480 boomer fullscreen. 🙌\u003cbr\u003e\n\t\tThat \u003ccode\u003eScaleMode\u003c/code\u003e, though… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp id=\"hotkeys-2024-10-22\"\u003e\n\tAnd then, I was looking for one more small optional feature to complete the 9\u003csup\u003eth\u003c/sup\u003e push and came up with the idea of hotkeys that would allow changing any of these settings at any point. Ember2528 considered it the best one of my ideas, so I went ahead… but little did I know that moving these graphics settings out of the main menu would not only significantly reshape the architecture of my code, but also uncover more bugs in my code and even a replay-related one from the original game. Paraphrasing the release notes:\n\u003c/p\u003e\u003cblockquote style=\"white-space: unset;\"\u003e\n\tThe original game had three bugs that affected the configured difficulty setting when playing the Extra Stage or watching an Extra Stage replay. When returning to the main menu from an Extra Stage replay, the configured difficulty would be overridden with either\n\t\u003col\u003e\u003cli\u003e\n\t\tthe difficulty selected before the last time the Extra Stage's Weapon Select screen was entered, or\n\t\u003c/li\u003e\u003cli\u003e\n\t\tEasy, when watching the replay before having been to the Extra Stage's Weapon Select screen during one run of the program.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tAlso, closing the game window during the Extra Stage (both self-played and replayed) would override the configured difficulty with Hard (the internal difficulty level of the Extra Stage).\n\t\u003c/li\u003e\u003c/ol\u003e\n\u003c/blockquote\u003e\u003cp\u003e\n\tThis had always been slightly annoying during development as I'd been closing the game window quite a bit during the Extra Stage. But the true nature of this bug only became obvious once these hotkeys allowed graphics settings to be changed \u003ci\u003eduring\u003c/i\u003e the Extra Stage: pbg had been \u003ca href=\"https://github.com/nmlgc/ssg/blob/pbg/GIAN07/DEMOPLAY.CPP#L101-L107\"\u003ecreating a copy of the full configuration structure just because lives, bombs, the difficulty level, and the input flags need to be overwritten with their values from the replay file\u003c/a\u003e, only to then recover the user's values \u003ca href=\"https://github.com/nmlgc/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/GIAN07/DEMOPLAY.CPP#L291-L296\"\u003eby restoring the full structure to its state from before the replay\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tBut the award for the greatest annoyance goes to \u003ca href=\"https://github.com/libsdl-org/sdlwiki/pull/592\"\u003ethis SDL quirk that would reset a render target's clipping region when returning to raw framebuffer rendering\u003c/a\u003e, which causes sprites to suddenly appear in the two black 128-pixel sidebars for the one frame after such a change. As long as graphics settings were only available from the unclipped main menu, this quirk only required a single silly workaround of manually backing up and restoring the clipping region. But once hotkeys allowed these settings to be changed while SDL_Renderer clips all draw calls to the 384×480 playfield region, I had to deploy the same exact workaround in three additional places…\u0026nbsp;🥲 At least I wrote it in a way that allows it to be easily deleted if we ever update to SDL 3, where the team fixed the underlying issue.\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, I'm not at all confident in the resulting jumbled mess of imperative code and conditional branches, but at least it proved itself during the 1½ months this feature has existed on my machine. If it's any indication, the testers in the Seihou development Discord group thought it was fine at the beginning of October when there were still 8 bugs left to be discovered. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tAs for the mappings themselves: F10 and F11 cycle the window scaling factor or borderless fullscreen fit, F9 toggles the \u003ccode\u003eScaleMode\u003c/code\u003e described below, and F8 toggles the frame rate limiter. The latter in particular is very useful for not only benchmarking, but also as a makeshift fast-forward function for replays. \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/62\"\u003eWouldn't rewinding also be cool?\u003c/a\u003e\n\u003c/p\u003e\u003chr id=\"scalemodes-2024-10-22\"\u003e\u003cp\u003e\n\tSo we've ported everything the game draws, including its most tricky pixel-level effect, and added windowed modes and scaling on top. That only leaves screenshots and then the SDL backend work would be complete. Now \u003ci\u003ethat's\u003c/i\u003e where we just call \u003ccode\u003e\u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderReadPixels\"\u003eSDL_RenderReadPixels()\u003c/a\u003e\u003c/code\u003e and write the returned pixels into a file, right? We've been scaling the game with the very convenient \u003ccode\u003e\u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderSetLogicalSize\"\u003eSDL_RenderSetLogicalSize()\u003c/a\u003e\u003c/code\u003e, so I'd expect to get back the logical 640×480 image to match the original behavior of the screenshot key…\u003cbr\u003e\n\t…except that we don't? Why do we only get back the 640×480 pixels in the top-left corner of the game's scaled output, right before it hits the screen? How unfortunate – if SDL forces us to save screenshots at their scaled output resolution, we'd needlessly multiply the disk space that these \u003ca href=\"https://github.com/nmlgc/ssg/issues/54\"\u003euncompressed .BMP files\u003c/a\u003e take up. But even if we did compress them, there should be no technical reason to blow up the pixels of these screenshots past the logical size we specified…\n\u003c/p\u003e\u003cp\u003e\n\tTaking \u003ca href=\"https://github.com/libsdl-org/SDL/blob/378234437fa2a99224391d5578f46971e190c0b7/src/render/SDL_render.c#L2315-L2451\"\u003ea closer look at \u003ccode\u003eSDL_RenderSetLogicalSize()\u003c/code\u003e\u003c/a\u003e explains what's going on there. This function merely calculates a scale factor by comparing the requested logical size with the renderer's output size, as well as a viewport within the game window if it has a different aspect ratio than the logical size. Then, it's up to the SDL_Renderer frontend to multiply and offset the coordinates of each incoming vertex using these values.\u003cbr\u003e\n\tTherefore, \u003ccode\u003eSDL_RenderReadPixels()\u003c/code\u003e can't possibly give us back a 640×480 screenshot because there simply is no 640×480 framebuffer that could be captured. As soon as the draw calls hit the render API and \u003ci\u003ecould\u003c/i\u003e be captured, their coordinates have already been transformed into the scaled viewport.\n\u003c/p\u003e\u003cp\u003e\n\tThe solution is obvious: Let's just create that 640×480 image ourselves. We'd first render every frame at that resolution into a texture, and then scale that texture to the window size by placing it on a single quad. From a preservation standpoint, this is also the academically correct thing to do, as it ensures that the entire game is still rendered at its original pixel grid. That's why this \u003ci\u003eframebuffer scaling\u003c/i\u003e mode is the default, in contrast to the \u003ci\u003egeometry scaling\u003c/i\u003e that SDL comes with.\n\u003c/p\u003e\u003cp\u003e\n\tWith integer scaling factors and nearest-neighbor filtering, we'd expect the two approaches to deliver exactly identical pixels as far as sprite rendering is concerned. At fractional resolutions though, we can observe the first difference right in the menu. While geometry scaling always renders boxes with sharp edges, it noticeably darkens the text inside the boxes because it separately scales and alpha-blends each shadowed line of text on top of the already scaled pixels below – remember, \u003ca href=\"/blog/2023-08-01#text-2023-08-01\"\u003e📝 the shadow for each line is baked into the same sprite\u003c/a\u003e. Framebuffer scaling, on the other hand, doesn't work on layers and always blurs every edge, but consequently also blends together all pixels in a much more natural way:\n\u003c/p\u003e\u003cfigure style=\"width: 685px;\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tLook closer, and you can even see texture coordinate glitches at the edges of the individual text line quads.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-menu-Geometry.png?2dbed9c6\"\n\t\tdata-title=\"Geometry scaling\"\n\t\talt=\"Screenshot of the new Graphic option menu in the Shuusou Gyoku P0295 build, as rendered at a scale factor of 3.75× using geometry scaling, showing off both the sharp edges on boxes and the darker, individually-blended lines of text\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-menu-FrameBuf.png?322e1b9a\"\n\t\tdata-title=\"Framebuffer scaling\"\n\t\talt=\"Screenshot of the new Graphic option menu in the Shuusou Gyoku P0295 build, as rendered at a scale factor of 3.75× using framebuffer scaling, showing off the more natural bilinearly-filtered look that blends the entire screen together and results in brighter text\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSurprisingly though, we don't see much of a difference with the circles in the Weapon Select screen. If geometry scaling only multiplies and offsets vertices, shouldn't the lines along the 32-sided polygons still be just one pixel thick? As it turns out, SDL puts in quite a bit of effort here: \u003ca href=\"https://github.com/libsdl-org/SDL/blob/378234437fa2a99224391d5578f46971e190c0b7/src/render/SDL_render.c#L3201-L3202\"\u003eIt never actually uses the API's line primitive when scaling the output\u003c/a\u003e, but instead takes the endpoints, rasterizes the line on the CPU, and turns each point on the resulting line into a quad the size of the scale factor. Of course, this completely nullifies pbg's original intent of approximating circles with lines for performance reasons. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tThe result looks better and better the larger the window is scaled. On low fractional scale factors like 1.25×, however, lines end up looking truly horrid as the complete lack of anti-aliasing causes the 1.25×1.25-pixel point quads to be rasterized as 2 pixels rather than a single one at regular intervals:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 320px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-circles-1.25x-Geometry.png?a7e12fc5\"\n\t\tdata-title=\"Geometry scaling\"\n\t\twidth=\"320\"\n\t\talt=\"Screenshot of the Wide Shot selection in Shuusou Gyoku's Weapon Select screen, as rendered at a scale factor of 1.25× using geometry scaling, showing off the inconsistent rasterization of point quads without anti-aliasing\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-circles-1.25x-FrameBuf.png?c286595c\"\n\t\tdata-title=\"Framebuffer scaling\"\n\t\twidth=\"320\"\n\t\talt=\"Screenshot of the Wide Shot selection in Shuusou Gyoku's Weapon Select screen, as rendered at a scale factor of 1.25× using framebuffer scaling\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tAlso note how you can either have bright circle colors or bright text colors, but not both.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut once we move in-game, we can even spot differences at integer resolutions if we look closely at all the shapes and gradients. In contrast to lines, software-rasterizing triangles with different vertex colors would be significantly more expensive as you'd suddenly have to cover a triangle's entire filled area with point quads. But thanks to that filled nature, SDL doesn't have to bother: It can merely scale the vertex coordinates as you'd expect and pass them onto the driver. Thus, the triangles get rasterized at the output resolution and end up as smooth and detailed as the output resolution allows:\n\u003c/p\u003e\u003cfigure style=\"width: 1152px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-LLaser-Geometry.png?b76e4173\"\n\t\tdata-title=\"Geometry scaling\"\n\t\talt=\"Screenshot of a long laser used by Shuusou Gyoku's Extra Stage midboss, rendered at a scale factor of 3× and using geometry scaling for smooth edges at the scaled resolution\"\n\t\twidth=\"1152\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-LLaser-FrameBuf.png?51a5eaa2\"\n\t\tdata-title=\"Framebuffer scaling\"\n\t\twidth=\"1152\"\n\t\talt=\"Screenshot of a long laser used by Shuusou Gyoku's Extra Stage midboss, rendered at 640×480 and framebuffer-scaled to 3× of the original resolution\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tNote how the HP gauge, being a gradient, also looks smoother with geometry scaling, whereas the Evade gauge, being 9 additively-blended red boxes with decreasing widths, doesn't differ between the modes.\u003cbr\u003e\n\t\tFor an even smoother rendering, enable anti-aliasing in your GPU's control panel; SDL unfortunately doesn't offer an API-independent way of enabling it.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tYou might now either like geometry scaling for adding these high-res elements on top of the pixelated sprites, or you might hate it for blatantly disrespecting the original game's pixel grid. But the main reasons for implementing and offering both modes are technical: As we've learned earlier when porting the lens ball effect, render-to-texture support is \u003ci\u003etechnically\u003c/i\u003e not guaranteed in SDL 2, and creating an additional texture is technically a fallible operation. Geometry scaling, on the other hand, will always work, as it's \u003cspan class=\"hovertext\" title=\"Technically, it also adds several dynamic allocations to store all those point quad lists, but if *those* fail, we have bigger problems.\"\u003ejust additional arithmetic\u003c/span\u003e.\u003cbr\u003e\n\tIf geometry scaling does find its fans though, we can use it as a foundation for \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/73\"\u003efurther high-res improvements\u003c/a\u003e. After all, this mode can't \u003ci\u003eever\u003c/i\u003e deliver a pixel-perfect rendition of the original Direct3D output, so we're free to add whatever enhancements we like while any accuracy concerns would remain exclusive to framebuffer scaling.\n\u003c/p\u003e\u003cp\u003e\n\tJust don't use geometry scaling with fractional scaling factors. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e These look even worse in-game than they do in the menus: The glitching texture coordinates reveal both the boundaries of on-screen tiles as well as the edge pixels of adjacent tiles within the set, and the scaling can even discolor certain dithered transparency effects, what the…?!\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThat green color is supposed to be the color key of this sprite sheet… 🤨\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-Stage-3-1.5x-Geometry.png?4b3d34ff\"\n\t\tdata-title=\"Geometry scaling\"\n\t\talt=\"Screenshot of a frame of Shuusou Gyoku's Stage 3 intro, rendered at a scale factor of 1.5× using geometry scaling, showing off tons of tilemap texture coordinate glitches and the usually half-transparent shadow of Gates' plane showing up in a strange green color\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-Stage-3-1.5x-FrameBuf.png?55d7c396\"\n\t\tdata-title=\"Framebuffer scaling\"\n\t\talt=\"Screenshot of a frame of Shuusou Gyoku's Stage 3 intro, rendered at a scale factor of 1.5× using framebuffer scaling, with no graphical glitches\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith both scaling paradigms in place, we now have a screenshot strategy for every possible rendering mode:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eSoftware-rendering (i.e., \u003ca href=\"#lens-2024-10-22\"\u003eshowing the \u003cspan lang=\"ja\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e logo\u003c/a\u003e)?\u003c/i\u003e\u003cbr\u003e\n\tThis is the optimal case. We've already rendered everything into a system-memory framebuffer anyway, so we can just take that buffer and write it to a file.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eHardware-rendering at unscaled 640×480?\u003c/i\u003e\u003cbr\u003e\n\tRequires a transfer of the GPU framebuffer to the system-memory buffer we initially allocate for software rendering, but no big deal otherwise.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eHardware-rendering with framebuffer scaling?\u003c/i\u003e\u003cbr\u003e\n\tAs we've seen with the initial solution for the lens ball effect, flagging a texture as a render target thankfully always allows us to read pixels back from the texture, so this is identical to the case above.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003ci\u003eHardware-rendering with geometry scaling?\u003c/i\u003e\u003cbr\u003e\n\tThis is the initial case where we must indeed bite the bullet and save the screenshot at the scaled resolution because that's all we can get back from the GPU. Sure, we could software-scale the resulting image back to 640×480, but:\u003cul\u003e\n\t\t\u003cli\u003eThat would defeat the entire point of geometry scaling as it would throw away all the increased detail displayed in the screenshots above. Maybe that \u003ci\u003eis\u003c/i\u003e something you'd like to capture if you deliberately selected this scale mode.\u003c/li\u003e\n\t\t\u003cli\u003eIf we scaled back an image rendered at a fractional scaling factor, we'd lose every last trace of sharpness.\u003c/li\u003e\n\t\u003c/ul\u003e\u003cp\u003eThe only sort of reasonable alternative: We could respond to the keypress by setting up a parallel 640×480 software renderer, rendering the next frame in both hardware and software in parallel, and delivering the requested screenshot with a 1-frame lag. This might be closer to what players expect, but it would make quite a mess of this already way too stateful graphics backend. And maybe, the lag is even longer than 1 frame because we simultaneously have to recreate all active textures in CPU-accessible memory…\u003c/p\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003chr id=\"subpixels-2024-10-22\"\u003e\u003cp\u003e\n\tNow that we can take screenshots, let's take a few and compare our 640×480 output to pbg's original Direct3D backend to see how close we got. Certain small details might vary across all the APIs we can use with SDL_Renderer, but at least for Direct3D 9, we'd expect nothing less than a pixel-perfect match if we pass the exact same vertices to the exact same APIs. But something seems to be wrong with the SDL backend at the subpixel level with any triangle-based geometry, regardless of which rendering API we choose…\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 384px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-vertex-offset-original.png?a7443525\" data-title=\"pbg's Direct3D backend\" class=\"active\" width=\"384\" alt=\"Screenshot of the laser-heavy pattern of VIVIT-captured-'s first form, as rendered by pbg's original Direct3D backend\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-SH01-vertex-offset-SDL.png?ae8c2205\" data-title=\"SDL\" width=\"384\" alt=\"Screenshot of the laser-heavy pattern of VIVIT-captured-'s first form, as rendered by the initial state of the SDL graphics backend, showing slightly displaced vertices compared to pbg's original Direct3D backend\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tAs if each polygon was shifted slightly up and to the left…\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe culprit is found quickly: \u003ca href=\"https://github.com/libsdl-org/SDL/blob/b6fa4dc794d24a2b534b7336b4660791ddb3730d/src/render/direct3d/SDL_render_d3d.c#L872-L873\"\u003eSDL's Direct3D 9 driver displaces each vertex by (﻿-0.5,\u0026nbsp;-0.5﻿) after scaling\u003c/a\u003e, which is necessary for Direct3D to perfectly match the output of what \u003ca href=\"https://github.com/libsdl-org/SDL/blob/b6fa4dc794d24a2b534b7336b4660791ddb3730d/src/render/opengl/SDL_render_gl.c#L1013-L1014\"\u003eOpenGL\u003c/a\u003e, \u003ca href=\"https://github.com/libsdl-org/SDL/blob/b6fa4dc794d24a2b534b7336b4660791ddb3730d/src/render/direct3d11/SDL_render_d3d11.c#L1646-L1647\"\u003eDirect3D 11\u003c/a\u003e, \u003ca href=\"https://github.com/libsdl-org/SDL/blob/b6fa4dc794d24a2b534b7336b4660791ddb3730d/src/render/direct3d12/SDL_render_d3d12.c#L2242-L2243\"\u003eDirect3D 12\u003c/a\u003e, and \u003ca href=\"https://github.com/libsdl-org/SDL/blob/b6fa4dc794d24a2b534b7336b4660791ddb3730d/src/render/software/SDL_render_sw.c#L576-L577\"\u003ethe software renderer\u003c/a\u003e would display without this offset. SDL is probably right here, but still, pbg's code doesn't do this. So it's up to us to counteract this displacement by adding \u003ccode\u003e(1.0\u0026nbsp;/(2.0\u0026nbsp;*\u0026nbsp;scale))\u003c/code\u003e to every vertex. 🤷\n\u003c/p\u003e\u003cp id=\"lines-2024-10-22\"\u003e\n\tThe other, much trickier accuracy issue is the line rendering. We saw earlier that SDL software-rasterizes any lines if we geometry-scale, but we do expect it to use the driver's line primitive if we framebuffer-scale or regularly render at 640×480. And at one point, it did, until \u003ca href=\"https://github.com/libsdl-org/SDL/issues/5061\"\u003ethe SDL team discovered accuracy bugs in various OpenGL implementations\u003c/a\u003e and \u003ca href=\"https://github.com/libsdl-org/SDL/commit/09ece861d1f8015f4960ab5fcfc6cb599e979f3e\"\u003edecided to just \u003ci\u003ealways\u003c/i\u003e software-rasterize lines by default\u003c/a\u003e to achieve identical rendered images regardless of the chosen API. Just like with the half-pixel offset above, this \u003ci\u003eis\u003c/i\u003e the correct choice for new code, but the wrong one for accurately porting an existing Direct3D game.\u003cbr\u003e\n\tThankfully, you can opt into the API's native line primitive \u003ca href=\"https://github.com/libsdl-org/SDL/blob/4ca7a193484eaaf98d99f6d68aa0313dad16b72c/include/SDL_hints.h#L1859-L1874\"\u003evia SDL's hint system\u003c/a\u003e, but the emphasis here is on \u003ci\u003eAPI\u003c/i\u003e. This hint can still only ensure a pixel-perfect match if SDL renders via any version of Direct3D and you either use framebuffer scaling or no scaling at all. OpenGL will draw lines differently, and the software renderer just uses the same point rasterizing algorithm that SDL uses when scaling.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 795px;\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tPixels written into the framebuffer along the accurate outline, as we've covered above. Also note the slightly brighter color compared to the 3D-rendered variants.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe original Direct3D line rendering used in pbg's original code, touching a total of 568 pixels.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tOpenGL's line rendering gets close, but still puts 16 pixels into different positions. Still, 97.2% of points are accurate to the original game.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe result of SDL's software line rasterizer, which you'd still see in the P0295 build when using either the software renderer or geometry scaling with any API. Slightly more accurate than OpenGL in this particular case with only 14 diverging pixels, matching 97.5% of the original circle.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tAs another alternative, SDL also offers a mode that renders each line as two triangles. This method naturally scales to any scale factor, but ends up drawing slightly thicker diagonals. You can opt into this mode via \u003ca href=\"https://wiki.libsdl.org/SDL2/CategoryHints\"\u003eSDL's hint system\u003c/a\u003e by setting the environment variable \u003ccode\u003eSDL_RENDER_LINE_METHOD\u003c/code\u003e to \u003ccode\u003e3\u003c/code\u003e.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe triangle method would also fit great with the spirit of geometry scaling, rendering smooth high-res circles analogous to the laser examples we saw earlier. This is how it would look like with the game scaled to 3200×2400… yeah, maybe we do want the point list after all, you can clearly see the 32 corners at this scale.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-8-bit.png?bf8b5eca\" data-title=\"8-bit\" width=\"795\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by pbg's original 8-bit drawing code\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-Direct3D.png?6acb925c\" data-title=\"Direct3D\" width=\"795\" class=\"active\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by Direct3D's line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-OpenGL.png?4c58444a\" data-title=\"OpenGL\" width=\"795\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by OpenGL's line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-SDL-points.png?9f0ddade\" data-title=\"SDL points\" width=\"795\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by SDL's point-based line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-SDL-triangles-1x.png?c477d725\" data-title=\"SDL triangles (1×)\" width=\"795\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by SDL's triangle-based line drawing algorithm at a scale factor of 1×\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-weapon-circle-SDL-triangles-5x.png?9e577e61\" data-title=\"SDL triangles (5×)\" width=\"795\" alt=\"Screenshot of Shuusou Gyoku's Homing Missile weapon option with the circular selection cursor as rendered by SDL's triangle-based line drawing algorithm at a geometry scale factor of 5×.\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tReplacing circles with point lists, as mentioned earlier, won't solve everything though, because Shuusou Gyoku also has plenty of non-circle lines:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 768px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-8-bit.png?aa122b6e\" data-title=\"8-bit\" width=\"768\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by pbg's original 8-bit drawing code\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-Direct3D.png?7616b1b0\" data-title=\"Direct3D\" width=\"768\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by Direct3D's line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-OpenGL.png?79156149\" data-title=\"OpenGL\" width=\"768\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by OpenGL's line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-SDL-points.png?85f2e111\" data-title=\"SDL points\" width=\"768\" class=\"active\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by SDL's point-based line drawing algorithm\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-SDL-triangles-1x.png?7b4d111b\" data-title=\"SDL triangles 1×\" width=\"768\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by SDL's triangle-based line drawing algorithm at a scale factor of 1×\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-10-22-SH01-warning-lines-SDL-triangles-2x.png?edf0fa67\" data-title=\"SDL triangles 2×\" width=\"768\" alt=\"The final 5-instance frame of Shuusou Gyoku's pre-boss WARNING wireframe animation against a black background, as rendered by SDL's triangle-based line drawing algorithm at a geometry scale factor of 5×.\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e6884 pixels touched by the Direct3D line renderer, a 98.3% match by the OpenGL rasterizer with 119 diverging pixels, and a 97.9% match by the SDL rasterizer with 147 diverging pixels. Looks like OpenGL gets better the longer the lines get, making line render method #2 the better choice even on non-Direct3D drivers.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo yeah, this one's kind of unfortunate, but also very minor as both OpenGL's and SDL's algorithms are at least 97% accurate to the original game. For now, this does mean that you'll manually have to change SDL_Renderer's driver from the OpenGL default to any of the Direct3D ones to get those last 3% of accuracy. However, I strongly believe that everyone who does care at this level will eventually read this sentence. And if we ever actually want 100% accuracy across every driver, we can always \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/74\"\u003ereverse-engineer and reimplement the exact algorithm used by Direct3D as part of our game code\u003c/a\u003e.\n\u003c/p\u003e\u003chr id=\"future-2024-10-22\"\u003e\u003cp\u003e\n\tThat completes the SDL renderer port for now! As all the GitHub issue links throughout this post have already indicated, I could have gone even further, but this is a convincing enough state for a first release. And once I've added a Linux-native font rendering backend, removed the few remaining \u003ccode\u003e\u0026lt;windows.h\u0026gt;\u003c/code\u003e types, and compiled the whole thing with GCC or Clang as a 64-bit binary, this will be up and running on Linux as well.\n\u003c/p\u003e\u003cp id=\"kog-2024-10-22\"\u003e\n\tIf we take a step back and look at what I've actually ended up writing during these SDL porting endeavors, we see a piece of almost generic retro game input, audio, window, rendering, and scaling middleware code, on top of SDL 2. After a slight bit of additional decoupling, most of this work should be reusable for not only Kioh Gyoku, but even the eventual cross-platform ports of PC-98 Touhou.\u003cbr\u003e\n\tPerhaps surprisingly, I'm actually looking \u003ci\u003eforward\u003c/i\u003e to Kioh Gyoku now. That game \u003ci\u003eseems\u003c/i\u003e to require raw access to the underlying 3D API due to a few effects that \u003ci\u003eseem\u003c/i\u003e to involve a Z coordinate, but all of these are transformed in software just like the few 3D effects in Shuusou Gyoku. Coming from a time when \u003ca href=\"https://en.wikipedia.org/wiki/Transform,_clipping,_and_lighting\"\u003ehardware T\u0026L\u003c/a\u003e wasn't a ubiquitous standard feature on GPUs yet, both games don't even bother and only ever pass Z coordinates of 0 to the graphics API, thus staying within the scope of SDL_Renderer. The only true additional high-level features that Kioh Gyoku requires from a renderer are sprite rotation and scaling, \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_RenderCopyEx\"\u003ewhich SDL_Renderer conveniently supports as well\u003c/a\u003e. I remember some of my backers thinking that Kioh Gyoku was going to be a huge mess, but looking at its code and \u003ci\u003enot\u003c/i\u003e seeing a separate 8-bit render path makes me rather excited to be facing a fraction of Shuusou Gyoku's complexity. The 3D engine sure \u003ci\u003eseems\u003c/i\u003e featureful at the surface, and the hundreds of source files sure \u003ci\u003efeel\u003c/i\u003e intimidating, but a lot of the harder-to-port parts remained unused in the final game. Kind of ironic that pbg wrote a largely new engine for this game, but we're closer to porting it back to our own enhanced, now almost fully cross-platform version of the Shuusou Gyoku engine.\n\u003c/p\u003e\u003cp id=\"palettized-2024-10-22\"\u003e\n\tSpeaking of 8-bit render paths though, you might have noticed that I didn't even bother to port that one to SDL. This is certainly suboptimal from a preservation point of view; after all, pbg specifically highlights in the source code's \u003ca href=\"https://github.com/nmlgc/ssg/tree/pbg?tab=readme-ov-file#%E5%8F%82%E8%80%83%E3%81%BE%E3%81%A7%E3%81%AB\"\u003eREADME\u003c/a\u003e how the split between palettized 8-bit and direct-color 16-bit modes was a particularly noteworthy aspect of the period in time when this game was written:\n\u003c/p\u003e\u003cblockquote\u003e\u003cstrong\u003e8bit/16bitカラーの混在\u003c/strong\u003e、MIDI再生関連、浮動小数点数演算を避ける、あたりが懐かしポイントになるかと思います。\u003c/blockquote\u003e\u003cp\u003e\n\tTimes have changed though, and SDL_Renderer doesn't even expose the concept of \u003ci\u003erendering bit depth\u003c/i\u003e at the API level. \u003ca href=\"/blog/2022-12-31\"\u003e📝 If we remember the initial motivation for these Shuusou Gyoku mods\u003c/a\u003e, Windows ≥8 doesn't even support anything below 32-bit anymore, and neither do most of SDL_Renderer's hardware-accelerated drivers as far as texture formats are concerned. While support for 24-bit textures without an alpha channel is still relatively common, only the \u003ca href=\"https://github.com/libsdl-org/SDL/blob/bcf1397e330f38f6fe4cf1fec980acea4d37d8d1/src/video/directfb/SDL_DirectFB_video.c#L303-L376\"\u003eLinux DirectFB driver\u003c/a\u003e \u003ci\u003emight\u003c/i\u003e support 16-bit and 8-bit textures, and you'd have to go back to the \u003ca href=\"https://github.com/libsdl-org/SDL/blob/bcf1397e330f38f6fe4cf1fec980acea4d37d8d1/src/render/vitagxm/SDL_render_vita_gxm.c#L112-L113\"\u003ePlayStation Vita\u003c/a\u003e, \u003ca href=\"https://github.com/libsdl-org/SDL/blob/bcf1397e330f38f6fe4cf1fec980acea4d37d8d1/src/render/ps2/SDL_render_ps2.c#L692\"\u003ePlayStation 2\u003c/a\u003e, or the software renderer to find guaranteed 16-bit support.\n\u003c/p\u003e\u003cp\u003e\n\tTherefore, full software rendering would be our only option. And sure enough, SDL_Renderer does have the necessary palette mapping code required for software-rendering onto a palettized 8-bit surface in system memory. That would take care of accurately constraining this render path to its intended 256 colors, but we'd still have to upconvert the resulting image to 32-bit every frame and upload it to GPU for hardware-accelerated scaling. This raises the question of whether it's even worth it to have 8-bit rendering in the SDL port to begin with if it will be undeniably slower than the GPU-accelerated direct-color port. If you think it's still a worthwhile thing to have, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/67\"\u003ehere is the issue to invest in\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tIn the meantime though, there is a much simpler way of continuing to preserve the 8-bit mode. As usual, I've kept pbg's old DirectX graphics code working all the way through the architectural cleanup work, which makes it almost trivial to compile that old backend into a separate binary and continue preserving the 8-bit mode in that way.\u003cbr\u003e\n\tThis binary is also going to evolve into the upcoming Windows 98 backport, and will be accompanied by its own SDL DLL that throws out the Direct3D 11, 12, OpenGL\u0026nbsp;2, and WASAPI backends as they don't exist on Windows 98. I've already thrown out the SSE2 and AVX implementations of the BLAKE3 hash function in preparation, which explains the smaller binary size. These Windows 98-compatible binaries will obviously have to remain 32-bit, but I'm undecided on whether I should update the regular Windows build to a 64-bit binary or keep it 32-bit:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eGoing 64-bit would give Windows users easy access to both builds and could help with testing and debugging rare issues that only occur in \u003ci\u003eeither\u003c/i\u003e the 64-bit \u003ci\u003eor\u003c/i\u003e the 32-bit build, whereas\u003c/li\u003e\n\t\u003cli\u003estaying 32-bit would make it less likely for us to \u003ci\u003eactually\u003c/i\u003e break the 32-bit Windows build because all Windows users (and developers) would continue using it.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tI'm open to strong opinions that sway me in one or the other direction, but I'm not going to do both – unless, of course, someone subscribes for the continued maintenance of three Windows builds. 😛\n\u003c/p\u003e\u003cp id=\"sdl3-2024-10-22\"\u003e\n\tSpeaking about SDL, we'll probably want to \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/68\"\u003eupdate from SDL 2 to SDL 3\u003c/a\u003e somewhere down the line. It's going to be the future, cleans up the API in a few particularly annoying places, and adds a Vulkan driver to SDL_Renderer. Too bad that \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_MixAudio\"\u003ethe documentation still deters me from using the audio subsystem\u003c/a\u003e despite the significant improvements it made in other regards…\u003cbr\u003e\n\tFor now, I'm still staying on SDL 2 for two main reasons:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhile SDL 3 is bound to be more available on Linux distributions in the future, that's not the case \u003ci\u003eright now\u003c/i\u003e. Everyone is still waiting for its first stable release, and so it currently isn't packaged in any distribution repo outside the AUR from what I can tell. Wide Linux compatibility is the whole point of this port.\u003c/li\u003e\n\t\u003cli\u003eThe funding for a Windows 98 port of SDL 2 was obviously intended to help with other existing SDL 2 games and not just Shuusou Gyoku.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tFinally, I decided against a Japanese translation of the new menu options for now because the help text communicates too much important information. That will have to wait until we make the whole game translatable into other languages.\n\u003c/p\u003e\u003chr id=\"echo-2024-10-22\"\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2024-04-11\"\u003e📝 I promised to recreate the Sound Canvas VA packs\u003c/a\u003e once I know about the exact way real hardware handles the \u003ca href=\"/blog/2024-03-09#sysex-2024-03-09\"\u003e📝 invalid Reverb Macro messages in ZUN's MIDI files\u003c/a\u003e, and what better time to keep that promise than to tack it onto the end of an already long overdue delivery. For some reason, Sound Canvas VA exhibited several weird glitches during the re-rendering processes, which prompted some rather extensive research and validation work to ensure that all tracks generally sound like they did in the previous version of the packages. Figuring out \u003ca href=\"https://github.com/nmlgc/BGMPacks/blob/d7166c11c21b7034bd1a4e22c4eb178524ac70e1/sh01/MIDI%20recording%20fixes.sed#L5-L10\"\u003ewhy this patch was necessary\u003c/a\u003e could have certainly taken a push on its own…\n\u003c/p\u003e\u003cp\u003e\n\tInterestingly enough, all these comparisons of renderings against each other revealed that the fix only makes a difference in a lot fewer than the expected 34 out of 39 MIDIs. \u003ca href=\"https://github.com/nmlgc/BGMPacks/blob/2024-10-05/sh01/README%20Sound%20Canvas%20VA%20echo%20header.md\"\u003eOnly 19 tracks – 11 in the OST and 8 in the AST – actually sound different depending on the Reverb Macro\u003c/a\u003e, because the remaining 15 set the reverb effect's main level to 0 and are therefore unaffected by the fix.\u003cbr\u003e\n\tAnd then, there is the Stage 1 theme, which only activates reverb during a brief portion of its loop:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-Domino-o02-original.png?83ec549d\"\n\t\tdata-title=\"Original\"\n\t\talt=\"Screenshot of the all SysEx messages appearing in the original MIDI file of the OST version of フォルスストロベリー, Shuusou Gyoku's Stage 1 theme, as decoded by Domino\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-10-22-Domino-o02-fixed.png?c5aa65f6\"\n\t\tdata-title=\"Fixed\"\n\t\talt=\"Screenshot of the all SysEx messages appearing in the a SysEx-fixed MIDI file of the OST version of フォルスストロベリー, Shuusou Gyoku's Stage 1 theme, as decoded by Domino\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eAs visualized by \u003ca href=\"/blog/2024-03-09#sysex-2024-03-09\"\u003e📝 Domino\u003c/a\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThus, this track definitely counts toward the 11 with a distinct echo version. But comparing that version against the no-echo one reveals something truly mind-blowing: The Sound Canvas VA rendering only differs within exactly the 8 bars of the loop, and is bit-by-bit identical anywhere else. 🤯 This is why you use softsynths.\n\u003c/p\u003e\u003cfigure class=\"fullres\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003eThis is the OST version, but it works just as well with the AST.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThis is the OST version, but it works just as well with the AST.\n\t\u003c/div\u003e\u003cdiv\u003eSince the no-echo and echo BGM packs are aligned in both time and volume, you can reproduce this result – and explore the differences for any other track across both soundtracks – by simply phase-inverting a no-echo variant file and mixing it into the corresponding echo file. Obviously, this works best with the FLAC files.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tSince the no-echo and echo BGM packs are aligned in both time and volume, you can reproduce this result – and explore the differences for any other track across both soundtracks – by simply phase-inverting a no-echo variant file and mixing it into the corresponding echo file. Obviously, this works best with the FLAC files. Trying it with the lossy versions gets surprisingly close though, and simultaneously reveals the infamous Vorbis pre-echo on the drums.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-10-22-SH01-o02-no-echo.flac?3c1d4b0a\" data-waveform=\"/blog/static/audio/2024-10-22-SH01-o02-no-echo.png?4f3b02fd\" preload=\"none\" controls data-title=\"No echo\" loop\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-10-22-SH01-o02-echo.flac?59afe8f5\" data-waveform=\"/blog/static/audio/2024-10-22-SH01-o02-echo.png?33ec0e25\" preload=\"none\" controls data-title=\"Echo\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-10-22-SH01-o02-diff-FLAC.flac?53588835\" data-waveform=\"/blog/static/audio/2024-10-22-SH01-o02-diff-FLAC.png?5289bff0\" preload=\"none\" controls data-title=\"Difference (FLAC)\" loop\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-10-22-SH01-o02-diff-Vorbis.flac?a30e59f8\" data-waveform=\"/blog/static/audio/2024-10-22-SH01-o02-diff-Vorbis.png?a00c1843\" preload=\"none\" controls data-title=\"Difference (Vorbis)\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo yeah, the fact that ZUN enabled reverb by suddenly increasing the level for just this 8-bar piano solo erases any doubt about the panning delay having been a quirk or accident. There is no way this wasn't done intentionally; whether the SC-88Pro's default reverb is at 0 or 40 barely makes an audible difference with all the notes played in this section, and wouldn't have been worth the unfortunate chore of inserting another GS SysEx message into the sequence. That's enough evidence to relegate the previous no-echo Sound Canvas VA packs to a strictly unofficial status, and only preserve them for reference purposes. If you downloaded the earlier ones, you might want to update… or maybe not if you don't like the echo, it's all about personal preference at the end of the day.\n\u003c/p\u003e\u003cp\u003e\n\tWhile we're that deep into reproducibility, it makes sense to address another slight issue with the March release. Back then, I rendered \u003ca href=\"/blog/2024-03-09#bgmpacks-2024-03-09\"\u003e📝 our favorite three MIDI files, the AST versions of the three Extra Stage themes\u003c/a\u003e, with their original long setup area and then trimmed the respective samples at the \u003ci\u003eaudio\u003c/i\u003e level. But since the MIDI-only BGM pack features a shortened setup area at the \u003ci\u003eMIDI\u003c/i\u003e level, rendering these modified MIDI files yourself wouldn't give you back the exact waveforms. \u003ca href=\"/blog/2023-09-30#resampling-2023-09-30\"\u003e📝 As PCM behaves like a lollipop graph\u003c/a\u003e, any change to the position of a note at a tempo that isn't an integer factor of the sampling rate will most likely result in completely different samples and thus be uncomparable via simple phase-cancelling.\u003cbr\u003e\n\tIn our case though, all three of the tracks in question render with a slightly higher maximum peak amplitude when shortening their MIDI setup area. Normally, I wouldn't bother with such a fluctuation, but remember that \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Extra Stage theme\"\u003eシルクロードアリス\u003c/span\u003e is by far the loudest piece across both soundtracks, and thus defines the peak volume that every other track gets normalized to.\u003cbr\u003e\n\tBut wait a moment, doesn't this mean that there's maybe a setup area length that could yield a lower or even \u003ci\u003emuch\u003c/i\u003e lower peak amplitude? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAnd so I \u003ca href=\"https://docs.google.com/spreadsheets/d/1j8wy9SLrjCiD9C3SXRU9dr7p22k0S6cg1hhHUAQ8TYA/edit?gid=440063624#gid=440063624\"\u003etested all setup area lengths at regular intervals between our target 2-beat length and ZUN's original lengths\u003c/a\u003e, and indeed found a great solution: When manipulating the setup area of the Extra Stage theme to an exact length of 2850\u0026nbsp;MIDI pulses, the conversion process renders it with a peak amplitude of 1.900, compared to its previous peak amplitude of 2.130 from the March release. That translates to an extra +0.56\u0026nbsp;dB of volume tricked out of all other tracks in the AST! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Yeah, it's not much, but hey, at least it's not worse than what it used to be. The shipped MIDIs of the Extra Stage themes still don't correspond to the rendered files, but now this is at least documented \u003ca href=\"https://github.com/nmlgc/BGMPacks/blob/d7166c11c21b7034bd1a4e22c4eb178524ac70e1/sh01/MIDI%20recording%20fixes.sed#L12-L26\"\u003etogether with the MIDI-level patch to reproduce the exact optimal length of the setup area\u003c/a\u003e.\u003cbr\u003e\n\tStill, all that testing effort for tracks that, in my subjective opinion, don't even \u003ci\u003esound\u003c/i\u003e all that good… The resulting shrill resonant effects stick out like a sore thumb compared to the more basic General MIDI sound of every other track across both soundtrack variants. Once again, unofficial remixes \u003ca href=\"https://www.shrinemaiden.org/forum/index.php?topic=18989.msg1280873#msg1280873\"\u003esuch as Romantique Tp's one edit to \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Reimu's theme\"\u003e二色蓮花蝶　～ Ancients\u003c/span\u003e\u003c/a\u003e can be the only solution here.\n\tAs far as preservation is concerned, this is as good as it gets, and my job here is done.\n\u003c/p\u003e\u003cp\u003e\n\tThen again, now that I've further refined (and actually scripted) the loop construction logic, I'd love to also apply it to Kioh Gyoku's MIDI soundtrack once its codebase is operational. Obviously, there's much less of an incentive for putting SC-88Pro recordings back into that game given that Kioh Gyoku already comes with an official (and, dare I say, significantly more polished) waveform soundtrack. And even if there \u003ci\u003ewas\u003c/i\u003e an incentive, it might not extend to a separate Sound Canvas VA version: As frustrating as ZUN's sequencing techniques in the final three Shuusou Gyoku Extra Stage arrangements are when dealing with rendered output, the fact that he reserved a lot more setup space to fit the more detailed sound design of each Kioh Gyoku track is a \u003ci\u003egood thing\u003c/i\u003e as far as real-hardware playback is concerned. Consequently, \u003ca href=\"https://www.youtube.com/playlist?list=PLzdQcm51guT--RXwVI9hv9wmRz8Ws5Zg_\"\u003ethe Romantique Tp recordings\u003c/a\u003e suffer far less from \u003ca href=\"/blog/2024-03-09#controversy-2024-03-09\"\u003e📝 the SC-88Pro's processing lag issues\u003c/a\u003e, and thus might already constitute all the preservation anyone would ever want.\u003cbr\u003e\n\tOnce again though, generous MIDI setup space also means that Kioh Gyoku's MIDI soundtrack has \u003ca href=\"https://www.youtube.com/watch?v=WUU1fy_JLH4\"\u003elots of long and awkward pauses at the beginning of stages before the music starts\u003c/a\u003e. The two worst offenders here are\n\t\u003cspan lang=\"ja\" class=\"hovertext\" title=\"VIVIT's theme\"\u003e天鵞絨少女戦　～ Velvet Battle\u003c/span\u003e and \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Yuuka's theme\"\u003e桜花之恋塚　～ Flower of Japan\u003c/span\u003e, with a 3:429s pause each. So, preserving the MIDI soundtrack in its originally intended sound might still be a worthwhile thing to fund if only to get rid of those pauses. After all, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/10#issuecomment-1938245315\"\u003ewe can't ever safely remove these pauses at the MIDI level unless users promise that they use a GS-supporting device\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tWhat we \u003ci\u003ecan\u003c/i\u003e do as part of the game, however, is hotpatch the original MIDI files from Shuusou Gyoku's \u003ccode\u003eMUSIC.DAT\u003c/code\u003e with the Reverb Macro fix. This way, the fix is also available for people who want to listen to the OST through their own copy of Sound Canvas VA or a SC-8850 and don't want to download recordings. This isn't necessary for the AST because we can simply bake the fix into the MIDI-only BGM pack, but we can't do this for the OST due to copyright reasons. This hotpatch should be an option just because hotpatching MIDIs is rather insidious in principle, but it's enabled by default due to the evidence we found earlier.\u003cbr\u003e\n\tThe game \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/64\"\u003ecurrently pauses when it loses focus\u003c/a\u003e, which also \u003cspan class=\"hovertext\" title=\"Not 'stops' – since we don't send Note Off commands, we can simply turn up the volume again once focus returns to the game window and all previously playing notes will be playing again.\"\u003esilences\u003c/span\u003e any currently playing MIDI notes. Thus, we can verify the active reverb type by switching between the game and VST windows:\n\u003c/p\u003e\u003cfigure\u003e\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\tMaximum volume recommended.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tStill saying \u003ci\u003ePanning Delay\u003c/i\u003e, even though we obviously hear the default reverb. A clear bug in the Sound Canvas VA UI.\n\t\u003c/div\u003e\u003c/figcaption\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-10-22-SH01-SysEx-compat-enabled.webp?b5d337b2\" preload=\"none\" controls data-title=\"SC-88Pro effect compatibility\" loop data-active width=\"960\" height=\"720\" data-fps=\"60\" data-frame-count=\"306\" style=\"aspect-ratio: 960 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-10-22-SH01-SysEx-compat-enabled.avi?437bf21b\"\u003e\u003csource src=\"/blog/static/video/av1/2024-10-22-SH01-SysEx-compat-enabled.webm?9a6c6869\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-10-22-SH01-SysEx-compat-enabled.webm?ede48495\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-10-22-SH01-SysEx-compat-enabled.webm?2f202965\" type=\"video/webm\"\u003eVideo demonstration of the SC-88Pro effect compatibility option in the Shuusou Gyoku P0295 build with an overlaid Sound Canvas VA window, showing off the reverb rendering and UI response with the option enabled. \u003ca href=\"/blog/static/video/zmbv/2024-10-22-SH01-SysEx-compat-enabled.avi?437bf21b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-10-22-SH01-SysEx-compat-disabled.webp?d5f595f6\" preload=\"none\" controls data-title=\"Unpatched, original MIDI file\" loop width=\"960\" height=\"720\" data-fps=\"60\" data-frame-count=\"306\" style=\"aspect-ratio: 960 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-10-22-SH01-SysEx-compat-disabled.avi?67e00f29\"\u003e\u003csource src=\"/blog/static/video/av1/2024-10-22-SH01-SysEx-compat-disabled.webm?104cbe3b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-10-22-SH01-SysEx-compat-disabled.webm?3b2b2e78\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-10-22-SH01-SysEx-compat-disabled.webm?e00d0561\" type=\"video/webm\"\u003eVideo demonstration of the SC-88Pro effect compatibility option in the Shuusou Gyoku P0295 build with an overlaid Sound Canvas VA window, showing off the reverb rendering and UI response with the option disabled. \u003ca href=\"/blog/static/video/zmbv/2024-10-22-SH01-SysEx-compat-disabled.avi?67e00f29\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tAnything I forgot? Oh, right, download links:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0295\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0295\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/BGMPacks/releases/tag/2024-10-05\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Updated Sound Canvas BGM packs\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up: You decide! This delivery has opened up quite a bit of budget, so this would be a good occasion to take a look at something else while we wait for a few more funded pushes to complete the Shuusou Gyoku Linux port. With the previous price increases effectively increasing the monetary value of earlier contributions, it might not always be exactly obvious how much money is needed \u003ci\u003eright now\u003c/i\u003e to secure another push. So I took a slight bit out of the \u003ci\u003eAnything\u003c/i\u003e funds to add the exact € amount to the \u003ca href=\"/fundlog\"\u003ecrowdfunding log\u003c/a\u003e.\u003cbr\u003e\n\tIn the meantime, I'll see how far I can get with porting all of the previous SDL work back to Windows 98 within one push-equivalent microtransaction, and do some internal website work to address some long-standing pain points.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-11-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-07-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-10-22T05:18:43Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-07-09",
      "url": "https://rec98.nmlgc.net/blog/2024-07-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-10-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-04-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-07-09\"\u003e\u003ctime datetime=\"2024-07-09T11:30:16Z\"\u003e2024-07-09 11:30\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0002\"\u003eP0002\u003c/a\u003e\n\t\t\tBuild system improvements, part 2 (Preparations / Codebase cleanup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/87eed57...f131878\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0003\"\u003eP0003\u003c/a\u003e\n\t\t\tBuild system improvements, part 3 (Lua rewrite of the Tupfile / Tup bugfixes for MS-DOS Player)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f131878...d60fb3e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0004\"\u003eP0004\u003c/a\u003e\n\t\t\tBuild system improvements, part 4 (Merging the 16-bit build part into the Tupfile)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b86a2b1...62bd5b1\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0281\"\u003eP0281\u003c/a\u003e\n\t\t\tBuild system improvements, part 5 (MS-DOS Player bugfixes and performance tuning for Turbo C++ 4.0J)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/msdos-player/compare/07d1088...P0281\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0282\"\u003eP0282\u003c/a\u003e\n\t\t\tBuild system improvements, part 6 (Generating an ideal dumb batch script for 32-bit platforms)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d60fb3e...056085b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0283\"\u003eP0283\u003c/a\u003e\n\t\t\tBuild system improvements, part 7 (Researching and working around Windows 9x batch file limits)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/056085b...b86a2b1\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0284\"\u003eP0284\u003c/a\u003e\n\t\t\t#include cleanup, part 1/2 / Decompilation (TH04/TH05 .REC loading)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/62bd5b1...18b9cd7\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0285\"\u003eP0285\u003c/a\u003e\n\t\t\t#include cleanup, part 2/2 / Decompilation (TH02 MAIN.EXE High Score entry)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/18b9cd7...23fc9e7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eGhostPhanom, [Anonymous], Blue Bolt, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dosbox-x\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A DOSBox fork with support for PC-98 emulation.\"\u003edosbox-x\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#trials-2024-07-09 .packaged {\n\t\tfont-weight: bold;\n\t}\n\t#trials-2024-07-09 th:not(:last-child),\n\t#trials-2024-07-09 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\t#trials-2024-07-09 thead td:nth-child(2n + 1),\n\t#trials-2024-07-09 tbody td:nth-child(2n + 2) {\n\t\tbackground-color: var(--c-trial-bad);\n\t}\n\t#trials-2024-07-09 thead td:nth-child(2n + 2),\n\t#trials-2024-07-09 tbody td:nth-child(2n + 3) {\n\t\tbackground-color: var(--c-trial-good);\n\t}\n\n\t#argv-2024-07-09 td {\n\t\tfont-family: monospace;\n\t}\n\t#argv-2024-07-09 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tI'm 13 days late, but 🎉 ReC98 is now 10 years old! 🎉 \u003ca href=\"https://github.com/nmlgc/ReC98/commit/9e07c54aeeb4fcfa6381505990602c333c83ca6e\"\u003eOn June 26, 2014\u003c/a\u003e, I first tried exporting IDA's disassembly of TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e and reassembling and linking the resulting file back into a binary, and was amazed that it actually yielded an identical binary. Now, this doesn't \u003ci\u003eactually\u003c/i\u003e mean that I've spent 10 years working on this project; priorities have been shifting and continue to shift, and time-consuming mistakes were certainly made. Still, it's a good occasion to finally fully realize the good future for ReC98 that GhostPhanom invested in with the very first financial contribution back in 2018, deliver the last three of the first four reserved pushes, cross another piece of time-consuming maintenance off the list, and prepare the build process for hopefully the next 10 years.\u003cbr\u003e\n\tBut why did it take 8 pushes and over two months to restore feature parity with the old system? 🥲\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#prev-2024-07-09\"\u003eThe previous build system(s)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tupfile-2024-07-09\"\u003eMigrating the 16-bit build part to Tup\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#msdos-2024-07-09\"\u003eOptimizing MS-DOS Player\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#win32-2024-07-09\"\u003eContinued support for building on 32-bit Windows\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tiers-2024-07-09\"\u003eThe new tier list of supported build platforms\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#includes-2024-07-09\"\u003eCleaning up \u003ccode\u003e#include\u003c/code\u003e lists\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02_regist-2024-07-09\"\u003eTH02's High Score menu\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"prev-2024-07-09\"\u003e\u003cp\u003e\n\tThe original plan for ReC98's good future was quite different from what I ended up shipping here. Before I started writing the code for this website in August 2019, I focused on feature-completing the \u003ca href=\"https://activitypub.nmlgc.net/@rec98/statuses/01DJE866685JA15FXGR9FWP02M\"\u003eexperimental 16-bit DOS build system for Borland compilers\u003c/a\u003e that I'd been developing since 2018, and which would form the foundation of my internal development work in the following years. Eventually, I wanted to polish and publicly release this system as soon as people stopped throwing money at me. But as of November 2019, just one month after launch, the store kept selling out with everyone investing into all the flashier goals, so that release never happened.\n\u003c/p\u003e\u003cfigure style=\"width: 642px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-07-09-omftup-2019.webp?2f150a2c\" preload=\"none\" controls data-title=\"2019\" loop data-active width=\"642\" height=\"432\" data-fps=\"20\" data-frame-count=\"200\" style=\"aspect-ratio: 642 / 432\" data-lossless=\"/blog/static/video/zmbv/2024-07-09-omftup-2019.avi?47456546\"\u003e\u003csource src=\"/blog/static/video/av1/2024-07-09-omftup-2019.webm?797045c3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-07-09-omftup-2019.webm?52caf1e3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-07-09-omftup-2019.webm?d1168888\" type=\"video/webm\"\u003eScreencast of my internal 16-bit build system compiling a 2019 checkout of the ReC98 repo, demonstrating how the dependency checks performed back then. \u003ca href=\"/blog/static/video/zmbv/2024-07-09-omftup-2019.avi?47456546\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-07-09-omftup-2024.webp?2f150a2c\" preload=\"none\" controls data-title=\"2024\" loop width=\"642\" height=\"432\" data-fps=\"20\" data-frame-count=\"240\" style=\"aspect-ratio: 642 / 432\" data-lossless=\"/blog/static/video/zmbv/2024-07-09-omftup-2024.avi?f42fe011\"\u003e\u003csource src=\"/blog/static/video/av1/2024-07-09-omftup-2024.webm?b0563e56\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-07-09-omftup-2024.webm?ed5ceef6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-07-09-omftup-2024.webm?b1aa6fce\" type=\"video/webm\"\u003eScreencast of my internal 16-bit build system compiling a 2024 checkout of the ReC98 repo at P0280, demonstrating the additional support for batched compilation, but also significantly decreased dependency check times. \u003ca href=\"/blog/static/video/zmbv/2024-07-09-omftup-2024.avi?f42fe011\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eIn theory, this build system remains the optimal way of developing with old Borland compilers on a real PC-98 (or any other 32-bit single-core system) and outside of Borland's IDE, even after the changes introduced by this delivery. In practice though, you're soon going to realize that there are \u003ci\u003elots\u003c/i\u003e of issues I'd have to revisit in case any PC-98 homebrew developers are interested in funding me to finish and release this tool…\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe main idea behind the system still has its charm: Your build script is a regular C++ program that \u003ccode\u003e#include\u003c/code\u003es the build system as a static library and passes fixed structures with names of source files and build flags. By employing static structure initialization, even a 1994 Turbo C++ would let you define the whole build at compile time, although this certainly requires some dank preprocessor magic to remain anywhere near readable at ReC98 scale. 🪄 While this system does require a bootstrapping process, the resulting binary can then use the same dependency-checking mechanisms to recompile and overwrite \u003ci\u003eitself\u003c/i\u003e if you change the C++ build code later. Since DOS just simply loads an entire binary into RAM before executing it, there is no lock to worry about, and overwriting the originating binary is something you can just \u003ci\u003edo\u003c/i\u003e.\u003cbr\u003e\n\tLater on, the system also made use of \u003ci\u003ebatched compilation\u003c/i\u003e: By passing more than one source file to \u003ccode\u003eTCC.EXE\u003c/code\u003e, you get to avoid TCC's quite noticeable startup times, thus speeding up the build proportional to the number of translation units in each batch. Of course, this requires that every passed source file is supposed to be compiled with the same set of command-line flags, but that's a generally good complexity-reducing guideline to follow in a build script. I went even further and \u003ci\u003eenforced\u003c/i\u003e this guideline in the system itself, thus truly making per-file compiler command line switches considered harmful. Thanks to Turbo C++'s \u003ccode\u003e#pragma option\u003c/code\u003e, changing the command line isn't even necessary for the few unfortunate cases where parts of ZUN's code \u003ci\u003ewere\u003c/i\u003e compiled with inconsistent flags.\u003cbr\u003e\n\tI combined all these ideas with a general approach of \"targeting DOSBox\": By maximizing DOS syscalls and minimizing algorithms and data structures, we spend as much time as possible in DOSBox's native-code DOS implementation, which \u003ci\u003eshould\u003c/i\u003e give us a performance advantage over DOS-native implementations of MAKE that typically follow the opposite approach.\n\u003c/p\u003e\u003cp\u003e\n\tOf course, all this only matters if the system is correct and reliable at its core. Tup teaches us that it's fundamentally impossible to have a reliable generic build system without\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eaugmenting the build graph with all actual files read and written by each invoked build tool, which involves tracing all file-related syscalls, and\u003c/li\u003e\n\t\u003cli\u003epersistently serializing the full build graph every time the system runs, allowing later runs to detect \u003ci\u003eevery possible\u003c/i\u003e kind of change in the build script and rebuild or clean up accordingly.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tUnfortunately, the design limitations of my system only allowed half-baked attempts at solving both of these prerequisites:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eIf your build system is not supposed to be generic and only intended to work with specific tools that emit reliable dependency information, you can replace syscall tracing with a parser for those specific formats. This is what my build system was doing, reading dependency information out of each .OBJ file's \u003ca href=\"https://en.wikipedia.org/wiki/Object_Module_Format_(Intel)\"\u003eOMF COMENT record\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eSince DOS command lines are limited to 127 bytes, DOS compilers support reading additional arguments from \u003ci\u003eresponse files\u003c/i\u003e, typically indicated with an \u003ccode\u003e@\u003c/code\u003e next to their path on the command line. If we now put \u003ci\u003eevery\u003c/i\u003e parameter passed to TCC or TLINK into a response file \u003ci\u003eand\u003c/i\u003e leave these files on disk afterward, we've effectively serialized all command-line arguments of the entire build into a makeshift database. In later builds, the system can then detect changed command-line arguments by comparing the existing response files from the previous run with the new contents it would write based on the current build structures. This way, we still only recompile the parts of the codebase that are affected by the changed arguments, which is \u003cspan class=\"hovertext\" title=\"Adding the Makefile itself onto every single rule line always rebuilds everything, and is quite honestly a silly thing to do.\"\u003efundamentally impossible with Makefiles\u003c/span\u003e.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tBut this strategy only covers changes within each binary's compile or link arguments, and ignores the required deletions in \"the database\" when removing binaries between build runs. This is a non-issue as long as we keep decompiling on \u003ccode\u003emaster\u003c/code\u003e, but as soon as we switch between \u003ccode\u003emaster\u003c/code\u003e and similarly old commits on the \u003ccode\u003edebloated\u003c/code\u003e/\u003ccode\u003eanniversary\u003c/code\u003e branches, we can get very confusing errors:\n\u003c/p\u003e\u003cfigure style=\"width: 960px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-07-09-omftup-halfbaked-serialization.png?72d3ac97\" alt=\"Screenshot of a seemingly weird error in my 16-bit build system that complains about TH01's vector functions being undefined when linking REIIDEN.EXE, shown when switching between the `anniversary` and `master` branches.\" style=\"max-height: unset;\"\u003e\n\t\u003cfigcaption\u003eThe symptom is a calling convention mismatch: The two vector functions use \u003ccode\u003e__cdecl\u003c/code\u003e on \u003ccode\u003emaster\u003c/code\u003e and \u003ccode\u003epascal\u003c/code\u003e on \u003ccode\u003edebloated\u003c/code\u003e/\u003ccode\u003eanniversary\u003c/code\u003e. We've switched from \u003ccode\u003eanniversary\u003c/code\u003e (which compiles to \u003ccode\u003eANNIV.EXE\u003c/code\u003e) back to \u003ccode\u003emaster\u003c/code\u003e (which compiles to \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e) here, so the .obj file on disk still uses the \u003ccode\u003epascal\u003c/code\u003e calling convention. The build system, however, only checks the response files associated with the current target binary (\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e) and therefore assumes that the .obj files still reflect the (unchanged) command-line flags in the TCC response file associated with this binary. And if none of the inputs of these .obj files changed between the two branches, they aren't rebuilt after switching, even though they would need to be.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tApparently, there's also such a thing as \"too much batching\", because TCC would suddenly stop applying certain compiler optimizations at very specific places if too many files were compiled within a single process? At least you quickly remember which source files you then need to manually touch and recompile to make the binaries match ZUN's original ones again…\n\u003c/p\u003e\u003cp\u003e\n\tBut the final nail in the coffin was something I'd notice on every single build: 5 years down the line, even the performance argument wasn't convincing anymore. The strategy of minimizing emulated code still left me with an 𝑂(𝑛) algorithm, and with \u003ci\u003ethis entire thing still being single-threaded\u003c/i\u003e, there was no force to counteract the dependency check times as they grew linearly with the number of source files.\u003cbr\u003e\n\tAt P0280, each build run would perform a total of 28,130 file-related DOS syscalls to figure out which source files have changed and need to be rebuilt. At some point, this was bound to become noticeable even despite these syscalls being native, not to mention that they're still surrounded by emulator code that must convert their parameters and results to and from the DOS ABI. And with the increasing delays before TCC would do its actual work, the entire thing started feeling increasingly jankier.\n\u003c/p\u003e\u003cp\u003e\n\tWhile this system was waiting to be eventually finished, the public \u003ccode\u003emaster\u003c/code\u003e branch kept using the Makefile that dates back to early 2015. Back then, it didn't \u003ca href=\"https://github.com/nmlgc/ReC98/commit/ff94dce594cdd66931fde2221a367785ac4a5e22\"\u003etake\u003c/a\u003e \u003ca href=\"https://github.com/nmlgc/ReC98/commit/7836363019bc0f17253be0f794c40ec21965ad68\"\u003elong\u003c/a\u003e for me to abandon raw dumb batch files because Make was simply the most straightforward way of ensuring that the build process would abort on the first compile error.\u003cbr\u003e\n\tThe following years also proved that Makefile syntax is quite well-suited for expressing the build rules of a codebase at this scale. The built-in support for automatically turning long commands into response files was especially helpful because of how naturally it works together with batched compilation. Both of these advantages culminate in this wonderfully arcane incantation of ASCII special characters and syntactically significant linebreaks:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003etcc … @\u0026\u0026|\n$**\n|\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tWhich translates to \"take the filenames of all dependents of this explicit rule, write them into a temporary file with an autogenerated name, insert this filename into the \u003ccode\u003etcc\u0026nbsp;…\u0026nbsp;@\u003c/code\u003e command line, and delete the file after the command finished executing\". The \u003ccode\u003e@\u003c/code\u003e is part of TCC's command-line interface, the rest is all MAKE syntax.\n\u003c/p\u003e\u003cp\u003e\n\tBut \u003ca href=\"/blog/2020-09-03\"\u003e📝 as we all know by now\u003c/a\u003e, these surface-level niceties change nothing about Makefiles inherently being unreliable trash due to implementing none of the aforementioned two essential properties of a generic build system. Borland got \u003ca href=\"https://github.com/nmlgc/ReCBMake\"\u003e\u003ci\u003eso\u003c/i\u003e close to a correct and reliable implementation of autodependencies\u003c/a\u003e, but that would have just covered one of the two properties. Due to this unreliability, the old \u003ccode\u003ebuild16b.bat\u003c/code\u003e called Borland's \u003ccode\u003eMAKER.EXE\u003c/code\u003e with the \u003ccode\u003e-B\u003c/code\u003e flag, recompiling everything all the time. Not only did this leave modders with a much worse build process than I was using internally, but it also eventually got old for me to merge my internal branch onto \u003ccode\u003emaster\u003c/code\u003e before every delivery. Let's finally rectify that and work towards a single good build process for everyone.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"tupfile-2024-07-09\"\u003e\n\tAs you would expect by now, I've once again migrated to Tup's Lua syntax. Rewriting it all makes you realize once again how complex the PC-98 Touhou build process is: It has to cover 2 programming languages, 2 pipeline steps, and 3 third-party libraries, and currently generates a total of 39 executables, including the small programs I wrote for research. The final Lua code comprises over 1,300 lines – but then again, if I had written it in \u003ca href=\"/blog/2023-09-30#zig-2023-09-30\"\u003e📝 Zig\u003c/a\u003e, it would certainly be as long or even longer due to manual memory management. The \u003ca href=\"https://github.com/nmlgc/tupblocks\"\u003eTup building blocks I constructed for Shuusou Gyoku\u003c/a\u003e quickly turned out to be the wrong abstraction for a project that has no debug builds, but their \u003ca href=\"/blog/2023-09-30#tup-2023-09-30\"\u003e📝 basic idea of a branching tree of command-line options\u003c/a\u003e remained at the foundation of this script as well.\u003cbr\u003e\n\tThis rewrite also provided an excellent opportunity for finally dumping all the intermediate compilation outputs into a separate dedicated \u003ccode\u003eobj/\u003c/code\u003e subdirectory, finally leaving \u003ccode\u003ebin/\u003c/code\u003e nice and clean with only the final executables. I've also merged this new system into most of the public branches of the GitHub repo.\n\u003c/p\u003e\u003cp\u003e\n\tAs soon as I first tried to build it all though, I was greeted with \u003ca href=\"https://github.com/gittup/tup/pull/500\"\u003ea particularly nasty Tup bug\u003c/a\u003e. Due to how DOS specified file metadata mutation, MS-DOS Player has to open every file in a way that current Tup treats as a write access… but since unannotated file writes introduce the risk of a malformed build graph if these files are read by another build command later on, Tup providently deletes these files after the command finished executing. And by \u003ci\u003ethese files\u003c/i\u003e, I mean \u003ccode\u003eTCC.EXE\u003c/code\u003e as well as every one of its C library header files opened during compilation. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tDue to a minor unsolved question about a failing test case, my fix has not been merged yet. But even if it was, we're now faced with a problem: If you previously chose to set up Tup for ReC98 or \u003ca href=\"/blog/2022-09-04\"\u003e📝 Shuusou Gyoku\u003c/a\u003e and are maybe still running \u003ca href=\"/blog/2020-09-03\"\u003e📝 my 32-bit build from September 2020\u003c/a\u003e, running the new \u003ccode\u003ebuild.bat\u003c/code\u003e would in fact delete the most important files of your Turbo C++ 4.0J installation, forcing you to reinstall it or restore it from a backup. So what do we do?\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eShould my custom build get a special version number so that the surrounding batch file can fail if the version number of your installed Tup is lower?\u003c/li\u003e\n\t\u003cli\u003eOr do I just put a message somewhere, which some people invariably won't read?\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe easiest solution, however, is to just put a fixed Tup binary directly into the ReC98 repo. This not only allows me to make Tup mandatory for 64-bit builds, but also cuts out one step in the build environment setup that at least one person previously complained about. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e *nix users might not like this idea all too much (\u003ca href=\"https://blog.hiler.eu/win32-the-only-stable-abi/\"\u003eor do they?\u003c/a\u003e), but then again, TASM32 and the Windows-exclusive MS-DOS Player require Wine anyway. Running Tup through Wine as well means that there's only one \u003ccode\u003ePATH\u003c/code\u003e to worry about, and you get to take advantage of the tool checks in the surrounding batch file.\u003cbr\u003e\n\tIf you're one of those people who doesn't trust binaries in Git repos, the repo also links to \u003ca href=\"https://github.com/nmlgc/tup/releases/tag/P0003\"\u003einstructions for building this binary yourself\u003c/a\u003e. Replicating this specific optimized binary is slightly more involved than the classic \u003ckbd\u003e./configure \u0026\u0026 make \u0026\u0026 make install\u003c/kbd\u003e trinity, so having these instructions is a good idea regardless of the fact that Tup's GPL license requires it.\n\u003c/p\u003e\u003cp\u003e\n\tOne particularly interesting aspect of the Lua code is the way it handles sprite dependencies:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003eth04:branch(MODEL_LARGE):link(\"main\", {\n\t{ \"th04_main.asm\", extra_inputs = {\n\t\tth02_sprites[\"pellet\"],\n\t\tth02_sprites[\"sparks\"],\n\t\tth04_sprites[\"pelletbt\"],\n\t\tth04_sprites[\"pointnum\"],\n\t} },\n\t-- …\n}\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tIf build commands read from files that were created by other build commands, Tup requires these input dependencies to be spelled out so that it can arrange the build graph and \u003cspan class=\"hovertext\" title=\"Otherwise, running the build for the first time would require Tup to predict the future in order to determine the files each build command will be accessing – which gets even harder if the set of accessed files is determined by the contents of files written by other build commands. 😵\"\u003eparallelize the build correctly\u003c/span\u003e. We could simply put \u003ci\u003eevery\u003c/i\u003e sprite into a single array and automatically pass that as an extra input to every source file, but that would effectively split the build into a \"sprite convert\" and \"code compile\" phase. Spelling out every individual dependency allows such source files to be compiled as soon as possible, before (and in parallel to) the rest of the sprites they don't depend on. Similarly, code files without sprite dependencies can compile before the first sprite got  converted, or even before the sprite converter itself got compiled and linked, maximizing the throughput of the overall build process.\n\u003c/p\u003e\u003cp\u003e\n\tRunning a 30-year-old DOS toolchain in a parallel build system also introduces new issues, though. The easiest and recommended way of compiling and linking a program in Turbo C++ is a single \u003ccode\u003etcc\u003c/code\u003e invocation:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003etcc … main.cpp utils.cpp master.lib\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tThis performs a batched compilation of \u003ccode\u003emain.cpp\u003c/code\u003e and \u003ccode\u003eutils.cpp\u003c/code\u003e within a single TCC process, and then launches TLINK to link the resulting \u003ccode\u003e.obj\u003c/code\u003e files into \u003ccode\u003emain.exe\u003c/code\u003e, together with the C++ runtime library and any needed objects from \u003ccode\u003emaster.lib\u003c/code\u003e. The linking step works by TCC generating a TLINK command line and writing it into a response file with the fixed name \u003ccode\u003eturboc.$ln\u003c/code\u003e… which obviously can't work in a parallel build where multiple TCC processes will want to link different executables via the same response file.\u003cbr\u003e\n\tTherefore, we have to launch TLINK with a custom response file ourselves. This file is \u003ccode\u003eecho\u003c/code\u003e'd as a separate parallel build rule, and the Lua code that constructs its contents has to replicate TCC's logic for picking the correct C++ runtime \u003ccode\u003e.lib\u003c/code\u003e file for the selected memory model.\n\u003c/p\u003e\u003cfigure\u003e\u003cpre style=\"white-space: unset;\"\u003e\n\t-c -s -t c0t.obj obj\\th02\\zun_res1.obj obj\\th02\\zun_res2.obj, bin\\th02\\zun_res.com, obj\\th02\\zun_res.map, bin\\masters.lib emu.lib maths.lib ct.lib\n\u003c/pre\u003e\u003cfigcaption\u003eThe response file for TH02's \u003ccode\u003eZUN_RES.COM\u003c/code\u003e, consisting of the C++ standard library, two files of ZUN code, and master.lib.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhile this does add more string formatting logic, not relying on TCC to launch TLINK actually removes the one possible \u003ccode\u003ePATH\u003c/code\u003e-related error case I previously documented in the README. Back in 2021 when I first stumbled over the issue, it took a few hours of RE to figure this out. I don't like these hours to go to waste, so \u003ca href=\"https://gist.github.com/nmlgc/6229345c74d1a7d3c6c1b3e988beb0e9\"\u003ehere's a Gist\u003c/a\u003e, and here's the text replicated for SEO reasons:\n\u003c/p\u003e\u003cblockquote style=\"white-space: unset;\"\u003e\n\t\u003cp\u003e\u003cstrong\u003eIssue:\u003c/strong\u003e TCC compiles, but fails to link, with \u003csamp\u003eUnable to execute command 'tlink.exe'\u003c/samp\u003e\u003c/p\u003e\n\n\t\u003cp\u003e\u003cstrong\u003eCause:\u003c/strong\u003e This happens when invoking TCC as a compiler+linker, without the \u003ccode\u003e-c\u003c/code\u003e flag. To locate TLINK, TCC needlessly copies the \u003ccode\u003ePATH\u003c/code\u003e environment variable into a statically allocated 128-byte buffer. It then constructs absolute \u003ccode\u003etlink.exe\u003c/code\u003e filenames for each of the semicolon- or \u003ccode\u003e\\0\u003c/code\u003e-terminated paths, writing these into a buffer that immediately follows the 128-byte \u003ccode\u003ePATH\u003c/code\u003e buffer in memory. The search is finished as soon as TCC finds an existing file, which gives precedence to earlier paths in the \u003ccode\u003ePATH\u003c/code\u003e. If the search didn't complete until a potential \"final\" path that runs past the 128 bytes, the final attempted filename will consist of the part that still managed to fit into the buffer, followed by the previously attempted path.\u003c/p\u003e\n\n\t\u003cp\u003e\u003cstrong\u003eWorkaround:\u003c/strong\u003e Make sure that the \u003ccode\u003eBIN\\\u003c/code\u003e path to Turbo C++ is fully contained within the first 127 bytes of the \u003ccode\u003ePATH\u003c/code\u003e inside your DOS system. (The 128\u003csup\u003eth\u003c/sup\u003e byte must either be a separating \u003ccode\u003e;\u003c/code\u003e or the terminating \u003ccode\u003e\\0\u003c/code\u003e of the \u003ccode\u003ePATH\u003c/code\u003e string.)\u003c/p\u003e\n\u003c/blockquote\u003e\u003cp\u003e\n\tNow that DOS emulation is an integral component of the single-part build process, it even makes sense to \u003ci\u003ecompile our pipeline tools as 16-bit DOS executables and then emulate them as part of the build\u003c/i\u003e. Sure, it's technically slower, but realistically it doesn't matter: Our only current pipeline tools are \u003ca href=\"/blog/2020-07-09\"\u003e📝 the converter for hardcoded sprites\u003c/a\u003e and the \u003ca href=\"/blog/2020-09-16\"\u003e📝 \u003ccode\u003eZUN.COM\u003c/code\u003e generators\u003c/a\u003e, both of which involve very little code and are rarely run during regular development after the initial full build. In return, we get to drop that awkward dependency on the separate Borland C++ 5.5 compiler for Windows and \u003ca href=\"https://github.com/nmlgc/ReC98/blob/87eed57ade4e2abd0f12272e568fe65a3643aeb3/README.md#how-to-build\"\u003eyet another additional manual setup step\u003c/a\u003e. 🗑️ Once PC-98 Touhou becomes portable, we're probably going to require a modern compiler anyway, so you can now delete that one as well.\n\u003c/p\u003e\u003cp\u003e\n\tThat gives us perfect dependency tracking and minimal parallel rebuilds across the whole codebase! While MS-DOS Player is noticeably slower than DOSBox-X, it's not going to matter all too much; unless you change one of the more central header files, you're rarely if ever going to cause a full rebuild. Then again, given that I'm going to use this setup for at least a couple of years, it's worth taking a closer look at why exactly the compilation performance is so underwhelming …\n\u003c/p\u003e\u003chr id=\"msdos-2024-07-09\"\u003e\u003cp\u003e\n\tOn the surface, MS-DOS Player seems like the right tool for our job, with a lot of advantages over DOSBox:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt doesn't spawn a window that boots an entire emulated PC, but is instead\u003c/li\u003e\n\t\u003cli\u003eperfectly integrated into the Windows console. Using it in a modern developer console would allow you to click on a compile error and have your editor immediately open the relevant file and jump to that specific line! With DOSBox, this basic comfort feature was previously unthinkable.\u003c/li\u003e\n\t\u003cli\u003eHeck, Takeda Toshiya originally developed it to run the equally vintage LSI C-86 compiler on 64-bit Windows. Fixing any potential issues we'd run into would be well within the scope of the project.\u003c/li\u003e\n\t\u003cli\u003eIt consists of just a single comparatively small binary that we could just drop into the ReC98 repo. No manual setup steps required.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut once I began integrating it, I quickly noticed two glaring flaws:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003eBack in 2009, Takeda Toshiya chose to start the project by writing a custom DOS implementation from scratch. He was aware of DOSBox, but only adapted small tricky parts of its source code rather than \u003ci\u003estarting\u003c/i\u003e with the DOSBox codebase and ripping out everything he didn't need. This matches the more research-oriented nature that \u003ca href=\"http://takeda-toshiya.my.coocan.jp/\"\u003eall of his projects appear to follow\u003c/a\u003e, where the primary goal of writing the code is a personal understanding of the problem domain rather than a widely usable piece of software. MS-DOS Player is even the outlier in this regard, with Takeda Toshiya describing it as \u003cspan class=\"hovertext\" lang=\"ja\" title='\"might be unusually practical\"'\u003e\u003cq\u003e珍しく実用的かもしれません\u003c/q\u003e\u003c/span\u003e. I am definitely sympathetic to this mindset; heck, my old internal build system falls under this category too, being \u003ci\u003eso\u003c/i\u003e specialized and narrow that it made little sense to use it outside of ReC98. But when you apply it to emulators for niche systems, you end up with exactly the current PC-98 emulation scene, where there's no single universally good emulator because all of them have \u003ci\u003esome\u003c/i\u003e inaccuracy \u003ci\u003esomewhere\u003c/i\u003e. This scene is too small for you not to eventually become part of someone else's supply chain… 🥲\u003cbr\u003e\n\tEmulating DOS is a particularly poor fit for a research/\u003ca href=\"https://en.wikipedia.org/wiki/Not_invented_here\"\u003eNIH\u003c/a\u003e project because it's \u003ca href=\"https://www.hyrumslaw.com/\"\u003eHyrum's Law\u003c/a\u003e incarnate. With the lack of memory protection in Real Mode, programs could freely access internal DOS (and even BIOS) data structures if they only knew where to look, and \u003ca href=\"https://github.com/joncampbell123/dosbox-x/issues/769\"\u003efrequently did\u003c/a\u003e. It might \u003ci\u003elook\u003c/i\u003e as if \"DOS command-line tools\" just equals x86 plus \u003ca href=\"https://www.stanislavs.org/helppc/int_21.html\"\u003e\u003ccode\u003eINT 21h\u003c/code\u003e\u003c/a\u003e, but soon you'll also be emulating the BIOS, PIC, PIT, EMS, XMS, and probably a few more things, all with their individual quirks that \u003ci\u003esome\u003c/i\u003e application out there relies on. DOSBox simply had much more time to grow and mature and figure out all of these details by trial and error. If you start a DOS emulator from scratch, you're bound to duplicate all this research as people want to use your emulator to run more and more programs, until you've ended up with what's effectively a clone of DOSBox's exact logic. Unless, of course, if you draw a line somewhere and limit the scope of the DOS and BIOS emulation. But given how many people have wanted to use MS-DOS Player for running DOS TUIs in arbitrarily sized terminal windows with arbitrary fonts, that's not what happened. I guess it made sense for this use case before \u003ca href=\"https://github.com/joncampbell123/dosbox-x/releases/tag/dosbox-x-v0.83.8\"\u003eDOSBox-X gained a TTF output mode in late 2020\u003c/a\u003e? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAs usual, I wouldn't mention this if I didn't run into \u003ca href=\"https://github.com/nmlgc/msdos-player/commit/4b260a18774bce2131e7ff72316cecaf3b98079b\"\u003etwo\u003c/a\u003e \u003ca href=\"https://github.com/nmlgc/msdos-player/commit/9079b83a638c448d093da8f7a3b3d3788201b84e\"\u003ebugs\u003c/a\u003e when combining MS-DOS Player with Turbo C++ and Tup. Both of these originated from workarounds for inaccuracies in the DOS emulation that date back to MS-DOS Player's initial release and were thankfully no longer necessary with the accuracy improvements implemented in the years since.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eFor CPU emulation, MS-DOS Player can use either MAME's or Neko Project 21/W's x86 core, both of which are interpreters and won't win any performance contests. The NP21/W core is significantly better optimized and runs ≈41% faster, but still pales in comparison to DOSBox-X's dynamic recompiler. Running the same sequential commands that the P0280 Makefile would execute, the upstream 2024-03-02 NP21/W core build of MS-DOS Player would take \u003ctime\u003e128.509s\u003c/time\u003e to compile the entire ReC98 codebase on my system, whereas DOSBox-X's dynamic core manages the same in \u003ctime\u003e66.202s\u003c/time\u003e, or 94% faster.\u003c/p\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tGranted, even the DOSBox-X performance is much slower than we would like it to be. Most of it can be blamed on the awkward time in the early-to-mid-90s when Turbo C++ 4.0J came out. This was the time when DOS applications had long grown past the limitations of the x86 Real Mode and required \u003ca href=\"https://en.wikipedia.org/wiki/DOS_extender\"\u003eDOS extenders\u003c/a\u003e or \u003ca href=\"https://www.os2museum.com/wp/a-brief-history-of-unreal-mode/\"\u003eeven sillier hacks\u003c/a\u003e to actually use all the RAM in a typical system of that period, but Win32 didn't exist yet to put developers out of this misery. As such, this compiler not only requires at least a 386 CPU, but also brings its own DOS extender (\u003ccode\u003eDPMI16BI.OVL\u003c/code\u003e) plus a loader for said extender (\u003ccode\u003eRTM.EXE\u003c/code\u003e), both of which need to be emulated alongside the compiler, to the great annoyance of emulator maintainers 30 years later. Even \u003ca href=\"https://github.com/nmlgc/msdos-player#readme\"\u003eMS-DOS Player's README file\u003c/a\u003e notes how Protected Mode adds a lot of complexity and slowdown:\n\u003c/p\u003e\u003cblockquote\u003e8086 binaries are much faster than 80286/80386/80486/Pentium4/IA32 binaries.\nIf you don't need the protected mode or new mnemonics added after 80286,\nI recommend i86_x86 or i86_x64 binary.\u003c/blockquote\u003e\u003cp\u003e\n\tThe immediate reaction to these performance numbers is obvious: \u003cq\u003eLet's just put DOSBox-X's dynamic recompiler into MS-DOS Player, right?!\u003c/q\u003e 🙌 Except that once you look at DOSBox-X, you immediately get why Takeda Toshiya might have preferred to start from scratch. Its codebase is a historically grown tangled mess, requiring intimate familiarity \u003ci\u003eand\u003c/i\u003e a significant engineering effort to isolate the dynamic core in the first place. I did spend a few days trying to untangle and copy it all over into MS-DOS Player… only to be greeted with an infinite loop as soon as everything compiled for the first time. 😶 Yeah, no, that's bound to turn into a budget-exceeding maintenance nightmare.\n\u003c/p\u003e\u003cp\u003e\n\tInstead, let's look at squeezing at least some additional performance out of what we already have. A generic emulator for the entire CISCy instruction set of the 80386, with complete support for Protected Mode, but it's only supposed to run the subset of instructions and features used by a specific compiler and linker as fast as possible… wait a moment, that sounds like a use case for \u003ca href=\"https://learn.microsoft.com/en-us/cpp/build/profile-guided-optimizations\"\u003eprofile-guided optimization\u003c/a\u003e! This is the first time I've encountered a situation that would justify the required 2-phase build process and lengthy profile collection – after all, writing into some sort of database for every function call does slow down MS-DOS Player by roughly 15×. However, profiling just the compilation of our most complex translation unit (\u003ca href=\"/blog/2022-08-08\"\u003e📝 TH01 YuugenMagan\u003c/a\u003e) and the linking of our largest executable (TH01's \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e) should be representative enough.\u003cbr\u003e\n\tI'll get to the performance numbers later, but even the build output is quite intriguing. Based on this profile, Visual Studio chooses to optimize only 104 out of MS-DOS Player's 1976 functions for speed and the rest for size, shaving off a nice 109\u0026nbsp;KiB from the binary. Presumably, keeping rare code small is also considered kind of fast these days because it takes up less space in your CPU's instruction cache once it \u003ci\u003edoes\u003c/i\u003e get executed?\n\u003c/p\u003e\u003cp\u003e\n\tWith PGO as our foundation, let's run a performance profile and see if there are any further code-level optimizations worth trying out:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ci\u003eRemoving redundant \u003ccode\u003ememset()\u003c/code\u003e calls\u003c/i\u003e: MS-DOS Player is written in a very C-like style of C++, and initializes a bunch of its statically allocated data by \u003ccode\u003ememset()\u003c/code\u003eing it with \u003ccode\u003e00\u003c/code\u003e bytes at startup. This is strictly redundant even in C; Section 6.7.9/10 of the C standard mandates that all static data is zero-initialized by default. In turn, the program loaders of modern operating systems employ all sorts of paging tricks to reduce the CPU cost (and actual RAM usage!) of this initialization as much as possible. If you manually \u003ccode\u003ememset()\u003c/code\u003e afterward, you throw all these advantages out of the window.\u003cbr\u003e\n\tOf course, these calls would only ever show up among the top CPU consumers in a performance profile if a program uses a large amount of static data, but the hardcoded 32\u0026nbsp;MiB of emulated RAM in ≥i386-supporting builds definitely qualifies. Zeroing 32.8\u0026nbsp;MiB of memory makes up a significant chunk of the runtime of some of the shorter build steps and quickly adds up; a full rebuild of the ReC98 codebase currently spawns a total of 361 MS-DOS Player instances, totaling 11.5\u0026nbsp;GiB of needless memory writes.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eLimiting the emulated instruction set\u003c/i\u003e: NP21/W's x86 core emulates everything up to the SSE3 extension from 2004, but Turbo C++ 4.0J's x86 instruction set usage doesn't stretch past the 386. It doesn't even need the x87 FPU for compiling code that involves floating-point constants. Disabling all these unneeded extensions speeds up x86's infamously annoying instruction decoding, and also reduces the size of the MS-DOS Player binary by another 149.5\u0026nbsp;KiB. The source code already had macros for this purpose, and only needed \u003ca href=\"https://github.com/nmlgc/msdos-player/commit/37031ebda2f90ea3ebe75e0c03b17f79e24ac0f4\"\u003ea slight fix for the code to compile with these macros disabled\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eRemoving x86 paging\u003c/i\u003e: Borland's DOS extender uses segmented memory addressing even in Protected Mode. This allows us to remove the MMU emulation and the corresponding \u003ci\u003e\"are we paging\"\u003c/i\u003e check for every memory access.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eRemoving cycle counting\u003c/i\u003e: When emulating a whole system, counting the cycles of each instruction is important for accurately synchronizing the CPU with other pieces of hardware. As hinted above, MS-DOS Player does emulate and periodically update a few pieces of hardware outside the CPU, but we need none of them for a build tool.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eTesting Takeda Toshiya's optimizations\u003c/i\u003e: In a nice turn of events, Takeda Toshiya merged every single one of my bugfixes and optimization flags into his upstream codebase. He even agreed with my \u003ccode\u003ememset()\u003c/code\u003e and cycle counting removal optimizations, which are now part of all upstream builds as of 2024-06-24. For the 2024-06-27 build, he claims to have gone even further than my more minimal optimization, so let's see how these additional changes affect our build process.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eFurther risky optimizations\u003c/i\u003e: A lot of the remaining slowness of x86 emulation comes from the segmentation and protection fault checks required for every memory access. If we assume that the emulator only ever executes correct code, we can remove these checks and implement further shortcuts based on their absence.\u003cbr\u003e\n\tThe \u003ca href=\"https://www.scs.stanford.edu/05au-cs240c/lab/i386/LGS.htm\"\u003e\u003ccode\u003eL[DEFGS]S\u003c/code\u003e group of instructions that load a segment and offset register from a 32-bit \u003ccode\u003efar\u003c/code\u003e pointer\u003c/a\u003e, for example, are both frequently used in Turbo C++ 4.0J code and particularly expensive to emulate. Intel specified their Real Mode operation as loading the segment and offset part in two separate 16-bit reads. But if we assume that neither of those reads can fault, we can compress them into a single 32-bit read and thus only perform the costly address translation once rather than twice. Emulator authors are probably rolling their eyes at this gross violation of Intel documentation now, but it's at least worth a try to see just how much performance we could get out of it.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo, what do we get?\n\u003c/p\u003e\u003cfigure\u003e\u003ctable id=\"trials-2024-07-09\" class=\"numbers trials\"\u003e\u003cthead\u003e\n\t\u003ctr\u003e\n\t\t\u003cth rowspan=\"2\"\u003eMS-DOS Player build\u003c/th\u003e\n\t\t\u003cth colspan=\"2\"\u003eFull build \u003csmall\u003e(Pipeline + 5 games + research code)\u003c/small\u003e\u003c/th\u003e\n\t\t\u003cth colspan=\"2\"\u003eMedian translation unit + median link\u003c/th\u003e\n\t\t\u003cth colspan=\"2\"\u003e\u003ca href=\"/blog/2022-08-08\"\u003e📝 YuugenMagan\u003c/a\u003e compile + link\u003c/th\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eGeneric\u003c/td\u003e\u003ctd\u003ePGO\u003c/td\u003e\u003ctd\u003eGeneric\u003c/td\u003e\u003ctd\u003ePGO\u003c/td\u003e\u003ctd\u003eGeneric\u003c/td\u003e\u003ctd\u003ePGO\u003c/td\u003e\n\t\u003c/tr\u003e\n\u003c/thead\u003e\u003ctbody\u003e\n\t\u003ctr style=\"border-bottom: 2px solid black;\"\u003e\n\t\t\u003ctd\u003eMAME x86 core\u003c/td\u003e\n\t\t\u003ctd\u003e46.522s / 50.854s\u003c/td\u003e\u003ctd\u003e32.162s / 34.885s\u003c/td\u003e\u003ctd\u003e1.346s / 1.429s\u003c/td\u003e\u003ctd\u003e0.966s / 0.963s\u003c/td\u003e\u003ctd\u003e6.975s / 7.155s\u003c/td\u003e\u003ctd\u003e4.024s / 3.981s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eNP21/W core,\u003cbr\u003ebefore optimizations\u003c/td\u003e\n\t\t\u003ctd\u003e34.620s / 36.151s\u003c/td\u003e\u003ctd\u003e30.218s / 31.318s\u003c/td\u003e\u003ctd\u003e1.031s / 1.065s\u003c/td\u003e\u003ctd\u003e0.885s / 0.916s\u003c/td\u003e\u003ctd\u003e5.294s / 5.330s\u003c/td\u003e\u003ctd\u003e4.260s / 4.299s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eNo initial \u003ccode\u003ememset()\u003c/code\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e31.886s / 34.398s\u003c/td\u003e\u003ctd\u003e27.151s / 29.184s\u003c/td\u003e\u003ctd\u003e0.945s / 1.009s\u003c/td\u003e\u003ctd\u003e0.802s / 0.852s\u003c/td\u003e\u003ctd\u003e5.094s / 5.266s\u003c/td\u003e\u003ctd\u003e4.104s / 4.190s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eLimited instructions\u003c/td\u003e\n\t\t\u003ctd\u003e32.404s / 34.276s\u003c/td\u003e\u003ctd\u003e26.602s / 27.833s\u003c/td\u003e\u003ctd\u003e0.963s / 1.001s\u003c/td\u003e\u003ctd\u003e0.783s / 0.819s\u003c/td\u003e\u003ctd\u003e5.086s / 5.182s\u003c/td\u003e\u003ctd\u003e3.886s / 3.987s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eNo paging\u003c/td\u003e\n\t\t\u003ctd\u003e29.836s / 31.646s\u003c/td\u003e\u003ctd\u003e25.124s / 26.356s\u003c/td\u003e\u003ctd\u003e0.865s / 0.918s\u003c/td\u003e\u003ctd\u003e0.748s / 0.769s\u003c/td\u003e\u003ctd\u003e4.611s / 4.717s\u003c/td\u003e\u003ctd\u003e3.500s / 3.572s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr style=\"border-bottom: 2px solid black;\"\u003e\n\t\t\u003ctd\u003eNo cycle counting\u003c/td\u003e\n\t\t\u003ctd\u003e25.407s / 26.691s\u003c/td\u003e\u003ctd\u003e21.461s / \u003cspan class=\"packaged\"\u003e22.599s\u003c/span\u003e\u003c/td\u003e\u003ctd\u003e0.735s / 0.752s\u003c/td\u003e\u003ctd\u003e0.617s / \u003cspan class=\"packaged\"\u003e0.625s\u003c/span\u003e\u003c/td\u003e\u003ctd\u003e3.747s / 3.868s\u003c/td\u003e\u003ctd\u003e2.873s / \u003cspan class=\"packaged\"\u003e2.979s\u003c/span\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr style=\"border-bottom: 2px solid black;\"\u003e\n\t\t\u003ctd\u003e2024-06-27 build\u003c/td\u003e\n\t\t\u003ctd\u003e26.297s / 27.629s\u003c/td\u003e\u003ctd\u003e21.014s / 22.143s\u003c/td\u003e\u003ctd\u003e0.771s / 0.779s\u003c/td\u003e\u003ctd\u003e0.612s / 0.632s\u003c/td\u003e\u003ctd\u003e4.372s / 4.506s\u003c/td\u003e\u003ctd\u003e3.253s / 3.272s\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eRisky optimizations\u003c/td\u003e\n\t\t\u003ctd\u003e23.168s / 24.193s\u003c/td\u003e\u003ctd\u003e20.711s / 21.782s\u003c/td\u003e\u003ctd\u003e0.658s / 0.663s\u003c/td\u003e\u003ctd\u003e0.582s / 0.603s\u003c/td\u003e\u003ctd\u003e3.269s / 3.414s\u003c/td\u003e\u003ctd\u003e2.823s / 2.805s\u003c/td\u003e\n\t\u003c/tr\u003e\n\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\tMeasured on a 6-year-old 6-core Intel Core i5 8400T on Windows 11. The first number in each column represents the codebase before the \u003ca href=\"#includes-2024-07-09\"\u003e\u003ccode\u003e#include\u003c/code\u003e cleanup explained below\u003c/a\u003e, and the second one corresponds to \u003ca href=\"https://github.com/nmlgc/ReC98/commit/1e41fa06177bc101a8631f1a47d5936ec87f8cd1\"\u003ethis commit\u003c/a\u003e. All builds are 64-bit, 32-bit builds were ≈5% slower across the board. I kept the fastest run within three attempts; as Tup parallelizes the build process across all CPU cores, it's common for the long-running full build to take up to a few seconds longer depending on what else is running on your system. Tup's standard output is also redirected to a file here; its regular terminal output and nice progress bar will add more slowdown on top.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe key takeaways:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eBy merely disabling certain x86 features from MS-DOS Player and retaining the accuracy of the remaining emulation, we get speedups of ≈60% (full build), ≈70% (median TU), and ≈80% (largest TU).\u003c/li\u003e\n\t\u003cli\u003e≈25% (full build), ≈29% (median TU), and ≈41% (largest TU) of this speedup came from Visual Studio's profile-guided optimization, with no changes to the MS-DOS Player codebase.\u003c/li\u003e\n\t\u003cli\u003eThe effects of removing cycle counting are the biggest surprise. Between ≈17% and ≈23%, just for removing one subtraction per emulated instruction? Turns out that in the absence of a \"target cycle amount\" setting, the x86 emulation loop previously ran for \u003ca href=\"https://github.com/nmlgc/msdos-player/blob/34e9106d2f9b0462c9478b44a7e6a4d4a76728f9/np21_i386.cpp#L368\"\u003eonly a single cycle\u003c/a\u003e. This caused \u003ca href=\"https://github.com/nmlgc/msdos-player/blob/34e9106d2f9b0462c9478b44a7e6a4d4a76728f9/np21_i386.cpp#L374-L378\"\u003ethe PIC check to run after every instruction\u003c/a\u003e, followed by \u003ca href=\"https://github.com/nmlgc/msdos-player/blob/34e9106d2f9b0462c9478b44a7e6a4d4a76728f9/msdos.cpp#L19963-L19965\"\u003ePIT, serial I/O, keyboard, mouse, and CRTC update code every millisecond\u003c/a\u003e. Without cycle counting, the x86 loop actually keeps running until a CPU exception is raised or the emulated process terminates, skipping the hardware code during the vast majority of the program's execution time.\u003c/li\u003e\n\t\u003cli\u003eWhile Takeda Toshiya's changes in the 2024-06-27 build completely throw out the cycle counter and clean up process termination, they also reintroduce the hardware updates that made up the majority of the cycle removal speedup. This explains the results we're getting: The small speedup for full rebuilds is too insignificant to bother with and might even fall within a statistical margin of error, but the build slows down more and more the longer the emulated process runs. Compiling and linking YuugenMagan takes a whole 14% longer on generic builds, and ≈9-12% longer on PGO builds. I did another in-between test that just removed the x86 loop from the cycle removal version, and got exactly the same numbers. This just goes to show how much removing \u003cspan class=\"hovertext\" title=\"One to subtract from the remaining cycles, one to set them back to 1 before the loop\"\u003etwo writes to a fixed memory address per emulated instruction\u003c/span\u003e actually matters. Let's not merge back this one, and stay on top of 2024-06-24 for the time being.\u003c/li\u003e\n\t\u003cli\u003eThe risky optimizations of ignoring segment limits and speeding up 32-bit segment+offset pointer load instructions \u003ci\u003ecould\u003c/i\u003e yield a further speedup. However, most of these changes boil down to removing branches that would never be taken when emulating correct x86 code. Consequently, these branches get recorded as unlikely during PGO training, which then causes \u003ca href=\"https://devblogs.microsoft.com/cppblog/profile-guided-optimization-pgo-under-the-hood/\"\u003ethe profile-guided rebuild to rearrange the instructions on these branches in a way that favors the common case\u003c/a\u003e, leaving the rest of their effective removal to your CPU's branch predictor. As such, the 10%-15% speedup we can observe in generic builds collapses down to 2%-6% in PGO builds. At this rate and with these absolute durations, it's not worth it to maintain what's strictly a more inaccurate fork of Neko Project 21/W's x86 core.\u003c/li\u003e\n\t\u003cli\u003eThe redundant header inclusions afforded by \u003ccode\u003e#include\u003c/code\u003e guards do in fact have a measurable performance cost on Turbo C++ 4.0J, slowing down compile times by 5%.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut how does this compare to DOSBox-X's dynamic core? Dynamic recompilers need some kind of cache to ensure that every block of original ASM gets recompiled only once, which gives them an advantage in long-running processes after the initial warmup. As a result, DOSBox-X compiles and links YuugenMagan in \u003ctime\u003e1.548s\u003c/time\u003e, ≈92% faster than even our optimized MS-DOS Player build. That percentage resembles the slowdown we were initially getting when comparing full rebuilds between DOSBox-X and MS-DOS Player, as if we hadn't optimized anything.\u003cbr\u003e\n\tOn paper, this would mean that DOSBox-X barely lost any of its huge advantage when it comes to single-threaded compile+link performance. In practice, though, this metric is supposed to measure a typical decompilation or modding workflow that focuses on repeatedly editing a single file. Thus, a more appropriate comparison would also have to add the aforementioned constant 28,130 syscalls that my old build system required to detect that \u003ci\u003ethis\u003c/i\u003e is the one file/binary that needs to be recompiled/relinked. The video at the top of this blog post happens to capture the best time (\u003ctime\u003e1.313s\u003c/time\u003e) I got for the detection process on DOSBox-X. This is almost as slow as the compilation and linking itself, and would have only gotten slower as we continue decompiling the rest of the games. Tup, on the other hand, performs its filesystem scan in a near-constant \u003ctime\u003e0.08s\u003c/time\u003e, \u003ca href=\"https://gittup.org/tup/build_system_rules_and_algorithms.pdf\"\u003ematching the claim in Section 4.7 of its paper\u003c/a\u003e, and thus shrinking the performance difference to ≈14% after all. Sure, merging the dynamic core would have been even better \u003cspan style=\"white-space: nowrap;\"\u003e(\u003c/span\u003e\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/contribution-ideas\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/a\u003e\u003c/span\u003e, anyone?), but this is good enough for now.\u003cbr\u003e\n\tJust like with Tup, I've also placed this optimized binary directly into the ReC98 repo and added the specific build instructions to the \u003ca href=\"https://github.com/nmlgc/msdos-player/releases/tag/P0281\"\u003eGitHub release page\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tI do have more far-reaching ideas for further optimizing Neko Project 21/W's x86 core for this specific case of repeated switches between Real Mode and Protected Mode while still retaining the interpreted nature of this core, but these already strained the budget enough.\u003cbr\u003e\n\tThe perhaps more important remaining bottleneck, however, is hiding in the actual DOS emulation. Right now, a Tup-driven full rebuild spawns a total of 361 MS-DOS Player processes, which means that we're booting an emulated DOS 361 times. This isn't as bad as it sounds, as \"booting DOS\" basically just involves initializing a bunch of internal DOS structures in \u003ca href=\"https://en.wikipedia.org/wiki/Conventional_memory\"\u003econventional memory\u003c/a\u003e to meaningful values. However, these structures also include a few environment variables like \u003ccode\u003ePATH\u003c/code\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/append\"\u003e\u003ccode\u003eAPPEND\u003c/code\u003e\u003c/a\u003e, or \u003ccode\u003eTEMP\u003c/code\u003e/\u003ccode\u003eTMP\u003c/code\u003e, which MS-DOS Player seamlessly integrates by translating them from their value on the Windows host system to the DOS 8.3 format. This could be one of the main reasons why MS-DOS Player is a native Windows program rather than being cross-platform:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eOn Windows, this path translation is as simple as calling \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getshortpathnamea\"\u003e\u003ccode\u003eGetShortPathNameA()\u003c/code\u003e\u003c/a\u003e, which returns a unique 8.3 name for every component along the path.\u003cbr\u003e\u003c/li\u003e\n\t\u003cli\u003eAlso, drive letters are an \u003ca href=\"https://www.ctyme.com/intr/rb-2570.htm\"\u003eintegral\u003c/a\u003e \u003ca href=\"https://www.ctyme.com/intr/rb-2588.htm\"\u003epart\u003c/a\u003e of the DOS \u003ccode\u003eINT 21h\u003c/code\u003e API, and Windows still uses them as well.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tHowever, the NT kernel doesn't actually \u003ci\u003euse\u003c/i\u003e drive letters either, and views them as just a legacy abstraction over its reality of volume GUIDs. Converting paths back and forth between these two views therefore requires it to communicate with a\n\t\u003ca href=\"https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/supporting-mount-manager-requests-in-a-storage-class-driver\"\u003emount point manager service\u003c/a\u003e, which can coincidentally also be observed in debug builds of Tup.\u003cbr\u003e\n\tAs a result, calling any path-retrieving API is a surprisingly expensive operation on modern Windows. When running a small sprite through our \u003ca href=\"/blog/2020-07-09\"\u003e📝 sprite converter\u003c/a\u003e, MS-DOS Player's boot process makes up 56% of the runtime, with 64% of that boot time (or 36% of the entire runtime) being spent on path translation. The actual x86 emulation to run the program only takes up 6.5% of the runtime, with the remaining 37.5% spent on initializing the multithreaded C++ runtime.\n\u003c/p\u003e\u003cp\u003e\n\tBut then again, the truly \u003ci\u003eoptimal\u003c/i\u003e solution would not involve MS-DOS Player at all. If you followed general video game hacking news in May, you'll probably remember the N64 community putting the concept of \u003ci\u003estatically recompiled game ports\u003c/i\u003e on the map. In case you're wondering where this seemingly sudden innovation came from and whether a reverse-engineered decompilation project like ReC98 is obsolete now, I wrote \u003ca href=\"/faq#recomp\"\u003ea new FAQ entry\u003c/a\u003e about why this hype, although justified, is at least in part misguided. tl;dr: None of this can be meaningfully applied to PC-98 games at the moment.\u003cbr\u003e\n\tOn the other hand, recompiling our \u003ci\u003ecompiler\u003c/i\u003e would not only be a reasonable thing to attempt, but \u003ci\u003eexactly the kind of problem that recompilation solves best\u003c/i\u003e. A 16-bit command-line tool has none of the pesky hardware factors that drag down the usefulness of recompilations when it comes to game ports, and a recompiled port could run even faster than it would on 32-bit Windows. Sure, it's not as flashy as a recompiled game, but if we got a few generous backers, it would still be a great investment into improving \u003ca href=\"https://github.com/M-HT/SR\"\u003ethe state of static x86 recompilation\u003c/a\u003e by simply having another open-source project in that space. Not to mention that it would be a great foundation for improving Turbo C++ 4.0J's code generation and optimizations, which would allow us to simplify lots of awkward pieces of ZUN code… 🤩\n\u003c/p\u003e\u003chr id=\"win32-2024-07-09\"\u003e\u003cp\u003e\n\tThat takes care of building ReC98 on 64-bit platforms, but what about the 32-bit ones we used to support? The previous split of the build process into a Tup-driven 32-bit part and a Makefile-driven 16-bit part sure was awkward and I'm glad it's gone, but it did give you the choice between 1) emulating the 16-bit part or 2) running both parts natively on 32-bit Windows. While \u003ca href=\"https://gittup.org/tup/win32/\"\u003eTup's upstream Windows builds are 64-bit-only\u003c/a\u003e, it made sense to \u003ca href=\"/blog/2020-09-03\"\u003e📝 compile a custom 32-bit version\u003c/a\u003e and thus turn any 32-bit Windows ≥Vista into the perfect build platform for ReC98. Older Windows versions that can't run Tup had to build the 32-bit part using a separately maintained dumb batch script created by \u003ccode\u003etup generate\u003c/code\u003e, but again, due to Make being trash, they were fully rebuilding the entire codebase every time anyway.\u003cbr\u003e\n\tDriving the entire build via Tup changes all of that. Now, it makes little sense to continue using 32-bit Tup:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWe need to DLL-inject into a 64-bit MS-DOS Player. Sure, we \u003ci\u003ecould\u003c/i\u003e compile a 32-bit build of MS-DOS Player, but why would we? If we look at current \u003ca href=\"https://web.archive.org/web/20240527074521/https://www.pcbenchmarks.net/os-marketshare.html\"\u003emarket\u003c/a\u003e \u003ca href=\"https://web.archive.org/web/20240628155943/https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam?platform=pc\"\u003eshares\u003c/a\u003e, nobody runs 32-bit Windows anymore, not even by accident. If you run 32-bit Windows in 2024, it's because you know what you're doing and made a conscious choice for the niche use case of natively running DOS programs. Emulating them defeats the whole point of setting up this environment to begin with.\u003c/li\u003e\n\t\u003cli\u003eIt \u003ci\u003ewould\u003c/i\u003e make sense if Tup could inject into DOS programs, but it can't.\u003c/li\u003e\n\t\u003cli\u003eAlso, as we're going to see later, requiring Windows ≥Vista goes in the opposite direction of what we want for a 32-bit build. The earlier the Windows version, the better it is at running native DOS tools.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis means that we could now only support 32-bit Windows via an even larger \u003ccode\u003etup generate\u003c/code\u003ed batch file. We'd have to move the MS-DOS Player prefix of the respective command lines into an environment variable to make Tup use the same rules for both itself and the batch file, but the result seems to work…\n\u003c/p\u003e\u003cp\u003e\n\t…but it's \u003ci\u003ereally\u003c/i\u003e slow, especially on Windows 9x. 🐌 If we look back at the theory behind my previous custom build system, we can already tell why: \u003ci\u003eEfficiently building ReC98 requires a completely different approach depending on whether you're running a typical modern multi-core 64-bit system or a vintage single-core 32-bit system.\u003c/i\u003e On the former, you'd want to parallelize the slow emulation as much as you can, so you maximize the amount of TCC processes to keep all CPU cores as busy as possible. But on the latter, you'd want the exact opposite – there, the biggest annoyance is the repeated startup and shutdown of the \u003ca href=\"https://en.wikipedia.org/wiki/Virtual_DOS_machine\"\u003eVDM\u003c/a\u003e, TCC, and its DOS extender, so you want to continue batching translation units into as few TCC processes as possible.\n\u003c/p\u003e\u003cp\u003e\n\tCMake fans will probably feel vindicated now, thinking \u003ci\u003e\"that sounds exactly like you need a meta build system 🤪\"\u003c/i\u003e. Leaving aside the fact that the output vomited by all of CMake's Makefile generators is a disgusting monstrosity that's far removed from addressing \u003ci\u003eany\u003c/i\u003e performance concerns, we sure \u003ci\u003ecould\u003c/i\u003e solve this problem by adding another layer of abstraction. But then, I'd have to rewrite my working Lua script into either C++ or (heaven forbid) Batch, which are the only options we'd have for bootstrapping without adding any further dependencies, and I \u003ci\u003ereally\u003c/i\u003e wouldn't want to do that. Alternatively, we could fork Tup and modify \u003ccode\u003etup generate\u003c/code\u003e to rewrite the low-level build rules that end up in Tup's database.\u003cbr\u003e\n\tBut why should we go for any of these if the Lua script already describes the build in a high-level declarative way? The most appropriate place for transforming the build rules is the Lua script itself…\n\u003c/p\u003e\u003cp\u003e\n\t… if there wasn't the slight problem of Tup forbidding file writes from Lua. 🥲 Presumably, this limitation exists because there is no way of replicating these writes in a \u003ccode\u003etup generate\u003c/code\u003ed dumb shell script, and it does make sense from that point of view.\u003cbr\u003e\n\tBut wait, printing to \u003ccode\u003estdout\u003c/code\u003e or \u003ccode\u003estderr\u003c/code\u003e works, and we always invoke Tup from a batch file anyway. You can now tell where this is going. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Hey, exfiltrating commands from a build script to the build system via standard I/O streams \u003ca href=\"https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script\"\u003eworks for Rust's Cargo too\u003c/a\u003e!\n\u003c/p\u003e\u003cp\u003e\n\tJust like Cargo, we want to add a sufficiently unique prefix to every line of the generated batch script to distinguish it from Tup's other output. Since Tup only reruns the Lua script – and would therefore print the batch file – if the script changed between the previous and current build run, we only want to overwrite the batch file if we got one or more lines. Getting all of this to work wasn't all too easy; we're once again entering the more awful parts of Batch syntax here, which apparently are so terrible that \u003ca href=\"https://bugs.winehq.org/show_bug.cgi?id=21227\"\u003eWine doesn't even bother to correctly implement parts of it.\u003c/a\u003e 😩\u003cbr\u003e\n\tMost importantly, we don't really want to redirect \u003ci\u003eany\u003c/i\u003e of Tup's standard I/O streams. Redirecting \u003ccode\u003estdout\u003c/code\u003e disables console output coloring and the pretty progress bar at the bottom, and looping over \u003ccode\u003estderr\u003c/code\u003e instead of \u003ccode\u003estdout\u003c/code\u003e in Batch is \u003ca href=\"https://stackoverflow.com/a/34488461\"\u003eincredibly awkward\u003c/a\u003e. Ideally, we'd run a second Tup process with a sub-command that would \u003ci\u003ejust\u003c/i\u003e evaluate the Lua script if it changed - and fortunately, \u003ccode\u003etup parse\u003c/code\u003e does exactly that. 😌\u003cbr\u003e\n\tIn the end, the optimally fast and \u003ccode\u003eERRORLEVEL\u003c/code\u003e-preserving solution involves two temporary files. But since creating files between two Tup runs causes it to reparse the Lua code, which would print the batch file to the unfiltered \u003ccode\u003estdout\u003c/code\u003e, we have to hide these temporary files from Tup by placing them into its \u003ccode\u003e.tup/\u003c/code\u003e database directory. 🤪\n\u003c/p\u003e\u003cp\u003e\n\tOn a more positive note, programmatically generating batches from single-file TCC rules turned out to be a great idea. Since the Lua code maps command-line flags to arrays of input files, it can also batch \u003ci\u003eacross\u003c/i\u003e binaries, surpassing my old system in this regard. This works especially well on the \u003ccode\u003edebloated\u003c/code\u003e and \u003ccode\u003eanniversary\u003c/code\u003e branches, which replace ZUN's little command-line flag inconsistencies with a single set of good optimization flags that every translation unit is compiled with.\n\u003c/p\u003e\u003cp\u003e\n\tTime to fire up some VMs then… only to see the build failing on Windows 9x with multiple unhelpful \u003csamp\u003eBad command or file name\u003c/samp\u003e errors. Clearly, the long \u003ccode\u003eecho\u003c/code\u003e lines that write our response files run up against some length limit in \u003ccode\u003ecommand.com\u003c/code\u003e and need to be split into multiple ones. Windows 9x's limit is larger than the 127 characters of DOS, that's for sure, and the exact number should just be one search away…\u003cbr\u003e\n\t…except that it's \u003ci\u003enot\u003c/i\u003e the \u003ca href=\"https://groups.google.com/g/alt.msdos.batch/c/z_H4JWPih-A/m/C499Mq4eJpAJ\"\u003e1024 characters recounted in a surviving newsgroup post\u003c/a\u003e. Sure, lines \u003ci\u003eare\u003c/i\u003e truncated to 1023 bytes and that off-by-one error is no big deal in this context, but that's not the whole story:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e: This not unrealistic command line is 137 bytes long and fails on Windows 9x?!\n\u003e echo -DA=1 2 3 a/b/c/d/1 a/b/c/d/2 a/b/c/d/3 a/b/c/d/4 a/b/c/d/5 a/b/c/d/6 a/b/c/d/7 a/b/c/d/8 a/b/c/d/9 a/b/c/d/10 a/b/c/d/11 a/b/c/d/12\nBad command or file name\n\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tWait, what, something about \u003ccode\u003e/\u003c/code\u003e being the \u003ca href=\"https://en.wikipedia.org/wiki/Command-line_interface#SwitChar\"\u003e\u003ccode\u003eSWITCHAR\u003c/code\u003e\u003c/a\u003e? And not even just that…\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e: Down to 132 bytes… and 32 \"assignments\"?\n\u003e echo a=0 b=1 c=2 d=3 e=4 f=5 g=6 h=7 i=8 j=9 k=0 l=1 m=2 n=3 o=4 p=5 q=6 r=7 s=8 t=9 u=0 v=1 w=2 x=3 y=4 z=5 a=0 b=1 c=2 d=3 e=4 f=5\nBad command or file name\n\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd what's perhaps the worst example:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e: 64 slashes. Works on DOS, works on `cmd.exe`, fails on 9x.\n\u003e echo ////////////////////////////////////////////////////////////////\nBad command or file name\n\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tMy complete set of test cases: \u003ca class=\"download\" href=\"/blog/static/2024-07-09-Win9x-batch-tokenizer-tests.bat?896d937a\" data-kb=\"2.9\"\u003e2024-07-09-Win9x-batch-tokenizer-tests.bat \u003c/a\u003e\n\tSo, time to load \u003ccode\u003ecommand.com\u003c/code\u003e into DOSBox-X's debugger and step through some code. 🤷 The earliest NT-based Windows versions were ported to a variety of CPUs and therefore received the then-all-new \u003ccode\u003ecmd.exe\u003c/code\u003e shell written in C, whereas Windows 9x's \u003ccode\u003ecommand.com\u003c/code\u003e was still built on top of the dense hand-written ASM code that originated in the very first DOS versions. Fortunately though, \u003ca href=\"https://github.com/microsoft/MS-DOS/tree/main/v4.0/src/CMD/COMMAND\"\u003eMicrosoft open-sourced one of the later DOS versions in April\u003c/a\u003e. This made it somewhat easier to cross-reference the disassembly even though the Windows 9x version significantly diverged in the parts we're interested in.\u003cbr\u003e\n\tAnd indeed: After truncating to 1023 bytes and parsing out any redirectors, each line is split into tokens \u003ci\u003earound\u003c/i\u003e whitespace and \u003ccode\u003e=\u003c/code\u003e signs and \u003ci\u003ebefore\u003c/i\u003e every occurrence of the \u003ccode\u003eSWITCHAR\u003c/code\u003e. These tokens are written into a statically allocated 64-element array, and once the code tries to write the 65\u003csup\u003eth\u003c/sup\u003e element, we get the \u003csamp\u003eBad command or file name\u003c/samp\u003e error instead.\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\" id=\"argv-2024-07-09\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e#\u003c/th\u003e\n\t\t\t\u003ctd\u003e0\u003c/td\u003e\n\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003ctd\u003e6\u003c/td\u003e\n\t\t\t\u003ctd\u003e7\u003c/td\u003e\n\t\t\t\u003ctd\u003e8\u003c/td\u003e\n\t\t\t\u003ctd\u003e9\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e11\u003c/td\u003e\n\t\t\t\u003ctd\u003e12\u003c/td\u003e\n\t\t\t\u003ctd\u003e13\u003c/td\u003e\n\t\t\t\u003ctd\u003e14\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eString\u003c/th\u003e\n\t\t\t\u003ctd\u003eecho\u003c/td\u003e\n\t\t\t\u003ctd\u003e-DA\u003c/td\u003e\n\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003ctd\u003ea\u003c/td\u003e\n\t\t\t\u003ctd\u003e/B\u003c/td\u003e\n\t\t\t\u003ctd\u003e/C\u003c/td\u003e\n\t\t\t\u003ctd\u003e/D\u003c/td\u003e\n\t\t\t\u003ctd\u003e/1\u003c/td\u003e\n\t\t\t\u003ctd\u003ea\u003c/td\u003e\n\t\t\t\u003ctd\u003e/B\u003c/td\u003e\n\t\t\t\u003ctd\u003e/C\u003c/td\u003e\n\t\t\t\u003ctd\u003e/D\u003c/td\u003e\n\t\t\t\u003ctd\u003e/2\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003eSwitch flag\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\t\u003ctd\u003e🚩\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tThe first few elements of \u003ccode\u003ecommand.com\u003c/code\u003e's internal argument array after calling the Windows 9x equivalent of \u003ccode\u003eparseline\u003c/code\u003e with my initial example string. Note how all the \"switches\" got capitalized and annotated with a flag, whereas the \u003ccode\u003e=\u003c/code\u003e sign no longer appears in either string or flag form.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tNeedless to say, this makes no sense. Both \u003ca href=\"https://stanislavs.org/helppc/int_21-4b.html\"\u003eDOS\u003c/a\u003e and \u003ca href=\"https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa\"\u003eWindows\u003c/a\u003e pass command lines as a single string to newly created processes, and since this tokenization is lossy, \u003ccode\u003ecommand.com\u003c/code\u003e will just have to pass the original string anyway. \u003ci\u003eIf\u003c/i\u003e your shell wants to handle tokenization at a central place, it should happen \u003ci\u003eafter\u003c/i\u003e it decided that the command matches a builtin that can actually make use of a pointer to the resulting token array – or better yet, as the first call of each builtin's code. Doing it \u003ci\u003ebefore\u003c/i\u003e is patently ridiculous.\u003cbr\u003e\n\tI don't know what's worse – the fact that Windows 9x blindly grinds each batch line through this tokenizer, or the fact that \u003ci\u003eno documentation of this behavior has survived on today's Internet, if any even ever existed\u003c/i\u003e. The closest thing I found was \u003ca href=\"https://web.archive.org/web/20081226032557/http://www.allenware.com/icsw/icsw200.htm\"\u003ethis page that doesn't exist anymore\u003c/a\u003e, and it also just contains a mere hint rather than a clear description of the issue. Even \u003ca href=\"https://www.robvanderwoude.com/batchcommands.php\"\u003ethe usual Batch experts who document everything else\u003c/a\u003e seem to have a blind spot when it comes to this specific issue. As do emulators: DOSBox and FreeDOS only reimplement the sane DOS versions of \u003ccode\u003ecommand.com\u003c/code\u003e, and Wine only reimplements \u003ccode\u003ecmd.exe\u003c/code\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tOh well. 71 lines of Lua later, the resulting batch file does in fact work everywhere:\n\u003c/p\u003e\u003cfigure style=\"width: 669px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThe clear performance winner at 11.15 seconds after the initial tool check, though sadly bottlenecked by strangely long TASM32 startup times. As for TCC though, even this performance is the \u003ci\u003eslowest\u003c/i\u003e a recompiled port would be. Modern compiler optimizations are probably going to shave off another second or two, and implementing support for \u003ccode\u003e#pragma once\u003c/code\u003e into the recompiled code will get us the aforementioned 5% on top.\u003cbr\u003e\n\t\tIf you run this on VirtualBox on modern Windows, make sure to disable Hyper-V to avoid the slower \u003ci\u003esnail execution mode\u003c/i\u003e. 🐢\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tBuilding in Windows XP under Hyper-V exchanges Windows 98's slow TASM32 startup times for slightly slower DOS performance, resulting in a still decent 13.4 seconds.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\t29.5 seconds?! Surely \u003ci\u003esomething\u003c/i\u003e is getting emulated here. And this is the \u003ci\u003ebest\u003c/i\u003e time I randomly got; \u003ca href=\"https://twitter.com/ReC98Project/status/1802932319520469347\"\u003emy initial preview recording took 55 seconds\u003c/a\u003e which is closer to DOSBox-X's dynamic core than it is to Windows 9x. Given how poorly 32-bit Windows 10 performs, Microsoft should have probably discontinued 32-bit Windows after 8 already. If any 16-bit program you could possibly want to run is either too slow or likely to exhibit other compatibility issues (\u003ca href=\"/blog/2022-12-31\"\u003e📝 Shuusou Gyoku, anyone?\u003c/a\u003e), the existence of 32-bit Windows 10 is nothing but a maintenance burden. \u003ci\u003eEspecially\u003c/i\u003e because Windows 10 simultaneously overhauled the console subsystem, which is bound to cause compatibility issues \u003ci\u003eanyway\u003c/i\u003e. \u003ca href=\"https://twitter.com/Nmlgc/status/1155892492954492930\"\u003eIt sure did for me back in 2019 when I tried to get my build system to work…\u003c/a\u003e\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-07-09-dumb-98.webp?10dabfc5\" preload=\"none\" controls data-title=\"Windows 98 SE\" loop data-active width=\"651\" height=\"331\" data-fps=\"20\" data-frame-count=\"278\" style=\"aspect-ratio: 651 / 331\" data-lossless=\"/blog/static/video/zmbv/2024-07-09-dumb-98.avi?144a06d1\"\u003e\u003csource src=\"/blog/static/video/av1/2024-07-09-dumb-98.webm?8bde9e5a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-07-09-dumb-98.webm?2207cf38\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-07-09-dumb-98.webm?59839af6\" type=\"video/webm\"\u003eScreencast of ReC98's new dumb 32-bit build batch script running natively under Windows 98 SE, finishing in 11.15 seconds. \u003ca href=\"/blog/static/video/zmbv/2024-07-09-dumb-98.avi?144a06d1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"35\" data-title=\"Tool check done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"258\" data-title=\"Build done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-07-09-dumb-XP.webp?a1b9ac35\" preload=\"none\" controls data-title=\"Windows XP\" loop width=\"669\" height=\"338\" data-fps=\"20\" data-frame-count=\"295\" style=\"aspect-ratio: 669 / 338\" data-lossless=\"/blog/static/video/zmbv/2024-07-09-dumb-XP.avi?3502d174\"\u003e\u003csource src=\"/blog/static/video/av1/2024-07-09-dumb-XP.webm?0b3a69ab\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-07-09-dumb-XP.webm?08bdf783\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-07-09-dumb-XP.webm?8636f873\" type=\"video/webm\"\u003eScreencast of ReC98's new dumb 32-bit build batch script running natively under 32-bit Windows XP, finishing in 13.4 seconds. \u003ca href=\"/blog/static/video/zmbv/2024-07-09-dumb-XP.avi?3502d174\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"7\" data-title=\"Tool check done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"275\" data-title=\"Build done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-07-09-dumb-10.webp?838b00cb\" preload=\"none\" controls data-title=\"Windows 10\" loop width=\"659\" height=\"332\" data-fps=\"20\" data-frame-count=\"625\" style=\"aspect-ratio: 659 / 332\" data-lossless=\"/blog/static/video/zmbv/2024-07-09-dumb-10.avi?7173cb68\"\u003e\u003csource src=\"/blog/static/video/av1/2024-07-09-dumb-10.webm?5756184d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-07-09-dumb-10.webm?83f7ec93\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-07-09-dumb-10.webm?04630ad8\" type=\"video/webm\"\u003eScreencast of ReC98's new dumb 32-bit build batch script running natively under 32-bit Windows 10, finishing in 29.5 seconds. \u003ca href=\"/blog/static/video/zmbv/2024-07-09-dumb-10.avi?7173cb68\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"15\" data-title=\"Tool check done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"605\" data-title=\"Build done\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut wait, there's more! The codebase now compiles on all 32-bit Windows systems I've tested, and yields binaries that are equivalent to ZUN's… \u003ci\u003eexcept\u003c/i\u003e on 32-bit Windows 10. 🙄 Suddenly, we're facing the exact same batched compilation bug from my custom build system again, with \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e being 16 bytes larger than it's supposed to be.\u003cbr\u003e\n\tLooks like I have to look into that issue after all, but figuring out the exact cause by debugging TCC would take ages again. Thankfully, trial and error quickly revealed a functioning workaround: \u003ci\u003eSeparating translation unit filenames in the response file with two spaces rather than one.\u003c/i\u003e Really, I couldn't make this up. This is the most ridiculous workaround for a bug I've encountered in a long time.\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003eecho -c  -I.  -O  -b-  -3  -Z  -d  -DGAME=4  -ml  -nobj/th04/  th04/op_main.cpp  th04/input_w.cpp  th04/vector.cpp  th04/snd_pmdr.c  th04/snd_mmdr.c  th04/snd_kaja.cpp  th04/snd_mode.cpp  th04/snd_dlym.cpp  th04/snd_load.cpp  th04/exit.cpp  th04/initop.cpp  th04/cdg_p_na.cpp  th04/snd_se.cpp  th04/egcrect.cpp  th04/bgimage.cpp  th04/op_setup.cpp  th04/zunsoft.cpp  th04/op_music.cpp  th04/m_char.cpp  th04/slowdown.cpp  th04/demo.cpp  th04/ems.cpp  th04/tile_set.cpp  th04/std.cpp  th04/tile.cpp\u003eobj\\batch014.@c\necho th04/playfld.cpp  th04/midboss4.cpp  th04/f_dialog.cpp  th04/dialog.cpp  th04/boss_exp.cpp  th04/stages.cpp  th04/player_m.cpp  th04/player_p.cpp  th04/hud_ovrl.cpp  th04/cfg_lres.cpp  th04/checkerb.cpp  th04/mb_inv.cpp  th04/boss_bd.cpp  th04/mpn_free.cpp  th04/mpn_l_i.cpp  th04/initmain.cpp  th04/gather.cpp  th04/scrolly3.cpp  th04/midboss.cpp  th04/hud_hp.cpp  th04/mb_dft.cpp  th04/grcg_3.cpp  th04/it_spl_u.cpp  th04/boss_4m.cpp  th04/bullet_u.cpp  th04/bullet_a.cpp  th04/boss.cpp  th04/boss_4r.cpp  th04/boss_x2.cpp  th04/maine_e.cpp  th04/cutscene.cpp\u003e\u003eobj\\batch014.@c\necho th04/staff.cpp\u003e\u003eobj\\batch014.@c\u003c/pre\u003e\n\t\u003cfigcaption\u003eThe TCC response file generation code for all current decompiled TH04 code, split into multiple \u003ccode\u003eecho\u003c/code\u003e calls based on the Windows 9x batch tokenizer rules and with double spaces between each parameter for added \u003cq\u003e\"safety\"\u003c/q\u003e. Would this also have been the solution for the batched compilation bugs I was experiencing with my old build system in DOSBox? I suddenly was unable to reproduce these bugs, so we won't know for the time being…\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"tiers-2024-07-09\"\u003e\u003cp\u003e\n\tHopefully, you've now got the impression that supporting any kind of 32-bit Windows build is way more of a liability than an asset these days, at least for this specific project. \u003ci\u003e\"Real hardware\"\u003c/i\u003e, \u003ci\u003e\"motivating a TCC recompilation\"\u003c/i\u003e, and \u003ci\u003e\"not dropping previous features\"\u003c/i\u003e really were the only reasons for putting up with the sheer jank and testing effort I had to go through. And I wouldn't even be surprised if real-hardware developers told me that the first reason doesn't actually hold up because compiling ReC98 on actual PC-98 hardware is slow enough that they'd rather compile it on their main machine and then transfer the binaries over some kind of network connection. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tI guess it also made for some mildly interesting blog content, but this was definitely the last time I bothered with such a wide variety of Windows versions without being explicitly funded to do so. If I ever get to recompile TCC, it will be 64-bit only by default as well.\n\u003c/p\u003e\u003cp\u003e\n\tInstead, let's have \u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/README.md#supported-build-platforms\"\u003ea tier list of supported build platforms that clearly defines what I am maintaining\u003c/a\u003e, with just the most convincing 32-bit Windows version in Tier 1. \u003ca href=\"https://twitter.com/ReC98Project/status/1804512907041910993\"\u003eInitially, that was supposed to be Windows 98 SE\u003c/a\u003e due to its superior performance, but that's just unreasonable if key parts of the OS remain undocumented and make no sense. So, XP it is.\u003cbr\u003e\n\t*nix fans will probably once again be disappointed to see their preferred OS in Tier 2. But at least, all we'd need for \u003ci\u003ethat\u003c/i\u003e to move up to Tier 1 is a CI configuration, contributed either via funding me or sending a PR. (Look, even more \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/contribution-ideas\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/a\u003e\u003c/span\u003e\u003cspan style=\"white-space: nowrap;\"\u003e!)\u003c/span\u003e\u003cbr\u003e\n\tGetting rid of the Wine requirement for a fully cross-platform build process wouldn't be \u003ci\u003etoo\u003c/i\u003e unrealistic either, but would require us to make a few quality decisions, as usual:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eDo we run the DOS tools by creating a cross-platform MS-DOS Player fork, or do we statically recompile them?\u003c/li\u003e\n\t\u003cli\u003eDo we replace 32-bit Windows TASM with the 16-bit DOS \u003ccode\u003eTASM.EXE\u003c/code\u003e or \u003ccode\u003eTASMX.EXE\u003c/code\u003e, which we then either run through our forked MS-DOS Player or recompile? This would further slow down the build and require us to get rid of these nice long non-8.3 filenames… 😕 I'd only recommend this after the looming librarization of ZUN's master.lib fork is completed.\u003c/li\u003e\n\t\u003cli\u003eOr do we try migrating to \u003ca href=\"https://github.com/JWasm/JWasm\"\u003eJWasm\u003c/a\u003e again? As an open-source assembler that aims for MASM compatibility, it's the closest we can get to TASM, but it's not a drop-in replacement by any means. I already \u003ca href=\"https://github.com/nmlgc/ReC98/compare/f54b85577d69585ae85893c7811f0b7e0f011728...5ad97a08ea10eaec9ab46e673d879443e2ac0712\"\u003etried in late 2014\u003c/a\u003e, but encountered too many issues and quickly abandoned the idea. Maybe it works better now that we have less ASM? In any case, this migration would only get easier the less ASM code we have remaining in the codebase as we get closer to the 100% finalization mark.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tY'know what I think would be the best idea for \u003ci\u003eright now\u003c/i\u003e, though? Savoring this new build system and spending an extended amount of time doing \u003ci\u003eactual decompilation or modding\u003c/i\u003e for a change. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr id=\"includes-2024-07-09\"\u003e\u003cp\u003e\n\tNow that even full rebuilds are decently fast, let's make use of that productivity boost by doing some urgent and far-reaching code cleanup that touches almost every single C++ source file. The most immediately annoying quirk of this codebase was the silly way each translation unit #included the headers it needed. Many years ago, I measured that repeatedly including the same header did significantly impact Turbo C++ 4.0J's compilation times, regardless of any include guards inside. As a consequence of this discovery, I slightly overreacted and decided to just not use \u003ci\u003eany\u003c/i\u003e include guards, ever. After all, this emulated build process is slow enough, and we don't want it to needlessly slow down even more! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e This way, redundantly including any file that adds more than just a few \u003ccode\u003e#define\u003c/code\u003e macros won't even compile, throwing lots of \u003csamp\u003eMultiple definition\u003c/samp\u003e errors.\u003cbr\u003e\n\tConsequently, the headers themselves #included almost nothing. Starting a new translation unit therefore always involved figuring and spelling out the transitive dependencies of the headers the new unit \u003ci\u003eactually\u003c/i\u003e wants to use, in a short trial-and-error process. While not too bad by itself, this was bound to become quite counterproductive once we get closer to porting these games: If some inlined function in a header needed access to, let's say, PC-98-specific I/O ports as an implementation detail, the header would have externalized this dependency to the top-level translation unit, which in turn made that that unit appear to contain PC-98-native code \u003ci\u003eeven if the unit's code itself was perfectly portable\u003c/i\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tBut once we start making some of these implicit transitive dependencies optional, it all stops being justifiable. Sometimes, \u003ccode\u003ea.hpp\u003c/code\u003e declared things that required declarations from \u003ccode\u003eb.hpp\u003c/code\u003e but these things are used so rarely that it didn't justify adding \u003ccode\u003e#include \"b.hpp\"\u003c/code\u003e to all translation units that \u003ccode\u003e#include \"a.hpp\"\u003c/code\u003e. So how about conditionally declaring these things based on previously #included headers? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e#if (defined(SUBPIXEL_HPP) \u0026\u0026 defined(PLANAR_H))\n\t// Sets the [tile_ring] tile at (x, y) to the given VRAM offset.\n\tvoid tile_ring_set_vo(subpixel_t x, subpixel_t y, vram_offset_t image_vo);\n#endif\u003c/pre\u003e\n\t\u003cfigcaption\u003eYou can \u003ci\u003emaybe\u003c/i\u003e do this in a project that consistently sorts the \u003ccode\u003e#include\u003c/code\u003e lists in every translation unit… err, no, don't do this, ever, it's awful. Just separate that declaration out into another header.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow that we've measured that the sane alternative of include guards comes with a performance cost of just 5% and we've further reduced its effective impact by parallelizing the build, it's worth it to take that cost in exchange for a tidy codebase without such surprises. From now on, every header file will \u003ccode\u003e#include\u003c/code\u003e its own dependencies and be a valid translation unit that must compile on its own without errors. In turn, this allows us to remove \u003ci\u003eat least\u003c/i\u003e 1,000 \u003ccode\u003e#include\u003c/code\u003es of transitive dependencies from \u003ccode\u003e.cpp\u003c/code\u003e files. 🗑️\u003cbr\u003e\n\tHowever, that 5% number was only measured after I reduced these redundant \u003ccode\u003e#include\u003c/code\u003es to their absolute minimum. So it still makes sense to only add include guards where they are absolutely necessary – i.e., transitively dependent headers included from more than one other file – and continue to (ab)use the \u003csamp\u003eMultiple definition\u003c/samp\u003e compiler errors as a way of communicating \u003ci\u003e\"you're probably #including too many headers, try removing a few\"\u003c/i\u003e. Certainly a less annoying error than \u003csamp\u003eUndefined symbol\u003c/samp\u003e.\n\u003c/p\u003e\u003chr id=\"th02_regist-2024-07-09\"\u003e\u003cp\u003e\n\tSince all of this went way over the 7-push mark, we've got some small bits of RE and PI work to round it all out. The \u003ccode\u003e.REC\u003c/code\u003e loader in TH04 and TH05 is completely unremarkable, but I've got at least a bit to say about TH02's High Score menu. I already decompiled \u003ccode\u003eMAINE.EXE\u003c/code\u003e's post-Staff Roll variant in 2015, so we were only missing the almost identical \u003ccode\u003eMAIN.EXE\u003c/code\u003e variant shown after a Game Over or when quitting out of the game. The two variants are similar enough that it mostly needed just a small bit of work to bring my old 2015 code up to current standards, and allowed me to quickly push TH02 over the 40% RE mark.\u003cbr\u003e\n\tFunctionally, the two variants only differ in two assignments, but ZUN once again chose to copy-paste the entire code to handle them. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This was one of ZUN's better copy-pasting jobs though – and honestly, I can't even imagine how you \u003ci\u003ewould\u003c/i\u003e mess up a menu that's entirely rendered on the PC-98's text RAM. It almost makes you wonder whether ZUN actually used the same \u003ccode\u003e#if ENDING\u003c/code\u003e preprocessor branching that my decompilation uses… until the visual inconsistencies in the alignment of the place numbers and the \u003cimg src=\"data:image/gif;base64,R0lGODlhUAAQAIABAAAAAP///yH5BAEKAAEALAAAAABQABAAAAKWjA+pcG3LnERxVnkfTHgv52VToIykWXEa1LCt2JkrqoJRTT05meHaK/uNSjGerXg6ApW0IcOH64lq04tP2LQ8paBlF6trOYNXoxmsJVO/6SzSCvM6iUE5kp08o+rxcFvWN0ZWNygIaBFTRoiHYbgodqQIeehmh3hHEYigxsn4Z8nTd6MpufKhFldGZ3NaOVc64wiaOVEAADs=\" alt=\"POINT\"\u003e and \u003cimg src=\"data:image/gif;base64,R0lGODlhIAAQAIABAAAAAP///yH5BAEKAAEALAAAAAAgABAAAAJAjB+gi70P2oEMqUmdnFjeZG3b142RqFmnSaJr8noZWtJwRbf2bcc73Ps5fD8iJyhz7Yg6Q/P4dFKMN5zsUXwWAAA7\" alt=\"ST\"\u003e labels clearly give it away as copy-pasted:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-07-09-TH02-High-Score-MAIN.png?54078365\" data-title=\"\u003ccode\u003eMAIN.EXE\u003c/code\u003e\" class=\"active\" alt=\"Screenshot of TH02's High Score screen as seen in MAIN.EXE when quitting out of the game, with scores initialized to show off the maximum number of digits and the incorrect alignment of the POINT and ST headers\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-07-09-TH02-High-Score-MAINE.png?5f10ff0f\" data-title=\"\u003ccode\u003eMAINE.EXE\u003c/code\u003e\" alt=\"Screenshot of TH02's High Score screen as seen in MAINE.EXE when entering a new high score after the Staff Roll, with scores initialized to show off the maximum number of digits and the incorrect alignment of the POINT header\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNext up: Starting the big Seihou summer! Fortunately, waiting two more months was worth it: In mid-June, Microsoft released a preview version of Visual Studio that, \u003ca href=\"https://developercommunity.visualstudio.com/t/Module-compilation-with-analyze-ignores/10627451\"\u003ein response to my bug report\u003c/a\u003e, finally, \u003ci\u003efinally\u003c/i\u003e makes C++ standard library modules fully usable. Let's clean up that codebase for real, and put this game into a window.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-10-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-04-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-07-09T11:30:16Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-04-24",
      "url": "https://rec98.nmlgc.net/blog/2024-04-24",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-07-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-04-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-04-24\"\u003e\u003ctime datetime=\"2024-04-24T13:14:41Z\"\u003e2024-04-24 13:14\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0280\"\u003eP0280\u003c/a\u003e\n\t\t\tTH03 RE (Coordinate transformations / Player entity movement / Global shared hitbox / Hit circles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/20bac82...87eed57\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, JonathKane, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dosbox-x\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A DOSBox fork with support for PC-98 emulation.\"\u003edosbox-x\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#overview-2024-04-24 tr\u003e:first-child {\n\t\ttext-align: right;\n\t\tborder-right: var(--table-border);\n\t}\n\n\t#overview-2024-04-24 tbody tr\u003e:nth-child(2) {\n\t\ttext-align: left;\n\t}\n\n\t#overview-2024-04-24 .or {\n\t\tdisplay: block;\n\t\tfont-weight: bold;\n\t\tfont-style: italic;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tTH03 gameplay! \u003ca href=\"/blog/2022-02-18\"\u003e📝 It's been over two years.\u003c/a\u003e People have been investing some decent money with the intention of eventually getting netplay, so let's cover some more foundations around player movement… and quickly notice that there's almost no overlap between gameplay RE and netplay preparations?\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#netplay-2024-04-24\"\u003eThe plan for TH03 netplay\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#netplay-2024-04-24\"\u003eBasic features\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#integration-2024-04-24\"\u003eTH03-specific integration\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#hitcirc-2024-04-24\"\u003eTH03's single hit circle\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"netplay-2024-04-24\"\u003e\u003cp\u003e\n\tThat makes for a fitting opportunity to think about what TH03 netplay would look like. Regardless of how we implement them into TH03 in particular, these features should \u003ci\u003ealways\u003c/i\u003e be part of the netcode:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\t\u003cp\u003e\u003ci\u003eData exchange protocol:\u003c/i\u003e \u003ca href=\"https://github.com/progre/junowen\"\u003eThe unofficial TH19 battle tool\u003c/a\u003e considered a few possibilities and ultimately chose WebRTC Data Channels. \u003ca href=\"https://www.youtube.com/watch?v=YRDMGuSNF70\u0026t=211s\"\u003eThe argument goes like this:\u003c/a\u003e\n\t\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eYou'd want UDP rather than TCP for both its low latency and its NAT hole-punching ability\u003c/li\u003e\n\t\t\u003cli\u003eHowever, raw UDP does not guarantee that the packets arrive in order, or that they even arrive at all\u003c/li\u003e\n\t\t\u003cli\u003eWebRTC implements these reliability guarantees on top of UDP in a modern package, providing the best of both worlds\u003c/li\u003e\n\t\t\u003cli\u003eNAT traversal via public or self-hosted STUN/TURN servers is built into the connection establishment protocol and \u003ca href=\"https://webrtc.org/getting-started/turn-server\"\u003eAPIs\u003c/a\u003e, so you don't even have to understand the underlying issue\u003c/li\u003e\n\t\u003c/ul\u003e\u003cp\u003e\n\t\tI'm not too deep into networking to argue here, and it clearly works for Ju.N.Owen. If we do explore other options, it would mainly be because I can't easily get something as modern as WebRTC to natively run on Windows 9x or DOS, if we decide to go for that route.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003e\u003ci\u003eMatchmaking:\u003c/i\u003e I like Ju.N.Owen's initial way of \u003ca href=\"https://youtube.com/clip/UgkxjUUb2lOcKvX51GlR-eM9vCme71hgI0Lx?si=o-5eweSxTmCnr_18\"\u003ecopy-pasting signaling codes into chat clients\u003c/a\u003e to establish a peer-to-peer connection without a dedicated matchmaking server. progre eventually implemented rooms \u003ca href=\"https://github.com/progre/junowen/tree/8f3f0d825a88ad98c353bf3ea9bd6565b1659d88/junowen-server\"\u003eon the AWS cloud\u003c/a\u003e, but signaling codes are still used for spectating and the Pure P2P mode. We'll probably copy the same evolution, with a slight preference for Pure P2P – if only because you would have to check a GDPR consent box before I can put the combination of your room name and IP address into a database. Server costs shouldn't be an issue at the scale I expect this to have.\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003e\u003ci\u003eRollback:\u003c/i\u003e In emulators, rollback netcode can be and \u003ca href=\"https://www.fightcade.com/about\"\u003ehas been\u003c/a\u003e implemented by keeping savestates of the last few frames together with the local player's inputs and then replaying the emulation with updated inputs of the remote player if a prediction turned out to be incorrect. This technique is a great fit for TH03 for two reasons:\u003cul\u003e\n\t\t\t\u003cli\u003eAll game state is contained within a relatively small bit of memory. The only heap allocations done in \u003ccode\u003eMAIN.EXE\u003c/code\u003e are the \u003ca href=\"/blog/2020-11-16\"\u003e📝 .MRS images for gauge attack portraits and bomb backgrounds\u003c/a\u003e, and the enemy scripts and formations, both of which remain constant throughout a round. All other state is statically allocated, which can reduce per-frame snapshots from the naive 640\u0026nbsp;KiB of conventional DOS memory to just the 37\u0026nbsp;KiB of \u003ccode\u003eMAIN.EXE\u003c/code\u003e's data segment. And that's the \u003ci\u003eupper\u003c/i\u003e bound – this number is only going to go down as we move towards 100% PI, figure out how TH03 uses all its static data, and get to consolidate all mutated data into an even smaller block of memory.\u003c/li\u003e\n\t\t\t\u003cli\u003eFor input prediction, we could even let the game's existing AI play the remote player until the actual inputs come in, guaranteeing perfect play until the remote inputs prove otherwise. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Then again… probably only while the remote player is not moving, because the chance for a human to replicate the AI's infamous erratic dodging is fairly low.\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003cp\u003e\n\t\tThe only issue with rollback in specifically a PC-98 emulator is its implications for performance. Rendering is way more computationally expensive on PC-98 than it is on consoles with hardware sprites, involving lots of memory writes to the disjointed 4 bitplane segments that make up the 128\u0026nbsp;KB framebuffer, and equally as many reads and bitshift operations on sprite data. TH03 lessens the impact somewhat thanks to most of its rendering being EGC-accelerated and thus running inside the emulator as optimized native code, but we'd still be emulating all the x86 code \u003ci\u003esurrounding\u003c/i\u003e the EGC accesses – from the emulator's point of view, it looks no different than game logic. Let's take my aging i5 system for example:\u003c/p\u003e\u003cul\u003e\n\t\t\t\u003cli\u003eWith the \u003ci\u003eScreen → No wait\u003c/i\u003e option, Neko Project 21/W can emulate TH03 gameplay at 260\u0026nbsp;FPS, or 4.6× its regular speed.\u003c/li\u003e\n\t\t\t\u003cli\u003eThis leaves room for each frame to contain 3.6 frames of rollback in addition to the frame that's supposed to be displayed,\u003c/li\u003e\n\t\t\t\u003cli\u003ewhich results in a maximum safe \u003cspan class=\"hovertext\" title=\"Duration of sending a packet from source to destination\"\u003enetwork latency\u003c/span\u003e of ≈63\u0026nbsp;ms, or a \u003cspan class=\"hovertext\" title=\"Round-trip time from source to destination and back\"\u003eping\u003c/span\u003e of ≈126\u0026nbsp;ms. According to \u003ca href=\"https://www.meter.net/tools/world-ping-test/\"\u003ethis site\u003c/a\u003e, that's enough for a smooth connection from Germany to any other place in Europe and even out to the US Midwest. At this ping, my system could still run the game without slowdown even if every single frame required a rollback, which is highly unlikely.\u003c/li\u003e\n\t\t\t\u003cli\u003eAny higher ping, however, could occasionally lead to a rollback queue that's too large for my system to process within a single frame at the intended 56.4 FPS rate. As a result, me playing anyone in the western US is highly likely to involve at least occasional slowdowns. Delaying inputs on purpose is the usual workaround, but isn't Touhou that kind of game series where people use vpatch to get rid of even the default input delay in the Windows games? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003cp\u003e\n\t\tSo we'd ideally want to put TH03 into an update-only mode that skips all rendering calls during re-simulation of rolled-back frames. Ironically, this means that netplay-focused RE would actually focus on the game's \u003ci\u003erendering\u003c/i\u003e code and ensure that it doesn't mutate any statically allocated data, allowing it to be freely skipped without affecting the game. Imagine palette-based flashing animations that are implemented by gradually mutating statically allocated values – these would cause wrong colors for the rest of the game if the animation doesn't run on every frame.\u003c/p\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp id=\"integration-2024-04-24\"\u003e\n\tThe integration of all of this into TH03 can be approached from several angles. Of course, as long as we don't port the game, netplay will still require a PC-98 emulator to run on modern systems. PC-98 emulation is typically regarded as difficult to set up and the additional configuration required for some of these methods would only make it harder. However, \u003ca href=\"https://yksoft1.github.io/dosboxxem-demo/\"\u003eyksoft1\u003c/a\u003e demonstrates that it doesn't have to be: By compiling the (potentially modified) PC-98 emulator to WebAssembly, running any of these non-native methods becomes as simple as opening a website. To stay legally safe, I wouldn't host the game myself, so you'd still have to drag your \u003ccode\u003eth03.hdi\u003c/code\u003e onto that browser tab. But if you're happy with playing in a browser, this would be as user-friendly as it gets.\n\u003c/p\u003e\u003cp\u003e\n\tHere's an overview of the various approaches with their most important pros and cons:\n\u003c/p\u003e\u003cfigure id=\"overview-2024-04-24\"\u003e\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003eRequirements\u003c/th\u003e\n\t\t\t\u003cth\u003ePretty in-game menus?\u003c/th\u003e\n\t\t\t\u003cth\u003eSupports non-Touhou PC-98 games?\u003c/th\u003e\n\t\t\t\u003cth\u003eWorks on PC-98 hardware?\u003c/th\u003e\n\t\t\t\u003cth\u003eNetcode location\u003c/th\u003e\n\t\t\t\u003cth\u003eTimeframe\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#emu-2024-04-24\"\u003eGeneric PC-98 netcode for emulators\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003eModded emulator + good CPU/RAM\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003eEmulator\u003c/td\u003e\n\t\t\t\u003ctd\u003eMonths\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#generic-2024-04-24\"\u003eEmulator-level netcode with game-specific hooks\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003eModded emulator\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003eEmulator\u003c/td\u003e\n\t\t\t\u003ctd\u003eMonths\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#compipes-2024-04-24\"\u003ePipes + standalone netplay tool\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003cul class=\"differences\"\u003e\n\t\t\t\t\u003cli\u003ePC-98 DOS with EMS/XMS\u003c/li\u003e\n\t\t\t\t\u003cli\u003eEmulator with named pipe support + separate netplay tool running on host \u003cspan class=\"or\"\u003eor\u003c/span\u003e real PC-98 + null modem cable + separate PC with netplay tool\u003c/li\u003e\n\t\t\t\u003c/ul\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003eExternal bridge\u003c/td\u003e\n\t\t\t\u003ctd\u003eMonths\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#win9x-2024-04-24\"\u003eNative PC-98 Windows 9x netcode\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003cul class=\"differences\"\u003e\n\t\t\t\t\u003cli\u003ePC-98 Windows 9x\u003c/li\u003e\n\t\t\t\t\u003cli\u003eEmulator with network support + TAP driver running on host \u003cspan class=\"or\"\u003eor\u003c/span\u003e real PC-98 + network card\u003c/li\u003e\n\t\t\t\u003c/ul\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003eWindows 9x bridge\u003c/td\u003e\n\t\t\t\u003ctd\u003eMonths\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#dos-2024-04-24\"\u003eNative PC-98 DOS netcode\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003cul class=\"differences\"\u003e\n\t\t\t\t\u003cli\u003ePC-98 DOS with EMS/XMS\u003c/li\u003e\n\t\t\t\t\u003cli\u003eEmulator with network support + TAP driver running on host \u003cspan class=\"or\"\u003eor\u003c/span\u003e real PC-98 + network card\u003c/li\u003e\n\t\t\t\u003c/ul\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003eGame logic\u003c/td\u003e\n\t\t\t\u003ctd\u003eMonths\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ca href=\"#port-2024-04-24\"\u003ePorting the game first\u003c/a\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003eAny halfway modern Windows or Linux system\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔️\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003e❌\u003c/td\u003e\n\t\t\t\u003ctd\u003eGame logic\u003c/td\u003e\n\t\t\t\u003ctd\u003eYears\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tDepending on what the backers prefer, we can go for one, a few, or all of these.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003col\u003e\n\t\u003cli id=\"emu-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003eGeneric PC-98 netcode for one or more emulators\u003c/i\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tThis is the most basic and puristic variant that implements generic netplay for PC-98 games in general by effectively providing remote control of the emulated keyboard and joypad. The emulator will be unaware of the game, and the game will be unaware of being netplayed, which makes this solution particularly interesting for the non-Touhou PC-98 scene, or competitive players who absolutely insist on using ZUN's original binaries and won't trust any of my modded game builds.\u003cbr\u003e\n\t\tApplied to TH03, this means that players would select the regular hot-seat \u003ci\u003e1P vs 2P\u003c/i\u003e mode and then initiate a match through a new menu in the emulator UI. The same UI must then provide an option to manually remap incoming key and button presses to the 2P controls (newly introducing remapping to the emulator if necessary), as well as blocking any non-2P keys. The host then sends an initial savestate to the guest to ensure an identical starting state, and starts synchronizing and rolling back inputs at VSync boundaries.\n\t\u003c/p\u003e\u003cp\u003e\n\t\tThis generic nature means that we don't get to include any of the TH03-specific rollback optimizations mentioned above, leading to the highest CPU and memory requirements out of all the variants. It sure is the easiest to implement though, as we get to freely use \u003ca href=\"https://github.com/paullouisageneau/libdatachannel\"\u003emodern C++ WebRTC libraries\u003c/a\u003e that are designed to work with the network stack of the underlying OS.\u003cbr\u003e\n\t\tI \u003ci\u003ecan\u003c/i\u003e try to build this netcode as a generic library that can work with any PC-98 emulator, but it would ultimately be up to the respective upstream developers to integrate it into official releases. Therefore, expect this variant to require separate funding and custom builds for each individual emulator codebase that we'd like to support.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli id=\"integrated-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003eEmulator-level netcode with game-specific hooks\u003c/i\u003e\n\t\u003c/p\u003e\n\t\t\u003cp\u003eTakes the generic netcode developed in 1) and adds the possibility for the game to control it via a special interrupt API. This enables several improvements:\u003c/p\u003e\u003cul\u003e\n\t\t\t\u003cli\u003eOnline matches could be initiated through new options in TH03's main menu rather than the emulator's UI.\u003c/li\u003e\n\t\t\t\u003cli\u003eThe game could communicate the memory region that should be backed up every frame, cutting down memory usage as described above.\u003c/li\u003e\n\t\t\t\u003cli\u003eThe exchanged input data could use the game's internal format instead of keyboard or joypad inputs. This removes the need for key remapping at the emulator level and naturally prevents the inherent issue of remote control where players could mess with each other's controls.\u003c/li\u003e\n\t\t\t\u003cli\u003eThe game could be aware of the rollbacks, allowing it to jump over its rendering code while processing the queue of remote inputs and thus gain some performance as explained above.\u003c/li\u003e\n\t\t\t\u003cli\u003eThe game could add synchronization points that block gameplay until both players have reached them, preventing the rollback queue from growing infinitely. This solves the issue of 1) not having any inherent way of working around desyncs and the resulting growth of the rollback queue. As an example, if one of the two emulators in 1) took, say, 2 seconds longer to load the game due to \u003ca href=\"https://www.reddit.com/r/intel/comments/jnwgtk/intel_driver_support_assistant_causing_sudden_and/?rdt=37820\"\u003ea random CPU spike caused by some bloatware on their system\u003c/a\u003e, the two players would be out of sync by 2 seconds for the rest of the session, forcing the faster system to render 113 frames every time an input prediction turned out to be incorrect.\u003cbr\u003e\n\t\t\tGood places for synchronization points include the beginning of each round, the \u003ci\u003eWARNING!! You are forced to evade / Your life is in peril\u003c/i\u003e popups that pause the game for a few frames anyway, and whenever the game is paused via the \u003ckbd\u003eESC\u003c/kbd\u003e key.\u003c/li\u003e\n\t\t\t\u003cli\u003eDuring such pauses, the game could then also block the resuming \u003ckbd\u003eESC\u003c/kbd\u003e key of the player who didn't pause the game.\u003c/li\u003e\n\t\t\u003c/ul\u003e\n\t\u003c/li\u003e\n\t\u003cli id=\"compipes-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003eEmulated serial port communicating over named pipes with a standalone netplay tool\u003c/i\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tThis approach would take the netcode developed in 2) out of the emulator and into a separate application running on the (modern) host OS, just like Ju.N.Owen or Adonis. The previous interrupt API would then be turned into a binary protocol communicated over the PC-98's serial port, while the rollback snapshots would be stored inside the emulated PC-98 in \u003ca href=\"https://en.wikipedia.org/wiki/Expanded_memory\"\u003eEMS\u003c/a\u003e or \u003ca\n\t\thref=\"https://en.wikipedia.org/wiki/Extended_memory\"\u003eXMS/Protected Mode\u003c/a\u003e memory. Netplay data would then move through these stages:\n\t\u003c/p\u003e\u003cfigure\u003e\u003cdiv\u003e\n\t\t🖥️ PC-98 game logic ⇄ Serial port ⇄ \u003cspan style=\"color: green\"\u003eEmulator ⇄ Named pipe ⇄ Netcode logic ⇄ WebRTC Data Channel ⇄\u003c/span\u003e Internet 🛜\n\t\u003c/div\u003e\u003cfigcaption\u003e\n\t\tAll \u003cspan style=\"color: green\"\u003egreen steps\u003c/span\u003e run natively on the host OS.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\t\tSending serial port data over named pipes is only a semi-common feature in PC-98 emulators, and would currently restrict netplay to Neko Project 21/W and \u003ca href=\"https://github.com/AZO234/NP2kai/blob/c2ca4046860264cb307e768f529f180caee5e224/windows/resources/932/np2.rc#L1595\"\u003eNP2kai on Windows\u003c/a\u003e. This is a pretty clean and generally useful feature to have in an emulator though, and emulator maintainers will be much more likely to include this than the custom netplay code I proposed in 1) and 2). \u003ca href=\"https://github.com/joncampbell123/dosbox-x/issues/4601\"\u003eDOSBox-X has an open issue that we could help implement\u003c/a\u003e, and the NP2kai Linux port would probably also appreciate a \u003ccode\u003emkfifo(3)\u003c/code\u003e implementation.\u003cbr\u003e\n\t\tThis could even work with emulators that only implement PC-98 serial ports in terms of, well, native Windows serial ports. This group currently includes Neko Project II fmgen, SL9821, T98-Next, and \u003ca href=\"https://lainnet.arcesia.net/repo/anex86ex_com.7z\"\u003erare bundles of Anex86 that replace MIDI support with COM port emulation\u003c/a\u003e. These would require separately installed and configured virtual serial port software in place of the named pipe connection, as well as support for actual serial ports in the netplay tool itself. In fact, this is the only way that die-hard Anex86 and T98-Next fans could enjoy \u003ci\u003eany\u003c/i\u003e kind of netplay on these two ancient emulators.\n\t\u003c/p\u003e\u003cp\u003e\n\t\t\u003ci\u003eIf\u003c/i\u003e it works though, it's the optimal solution for the emulated use case if we don't want to fork the emulator. From the point of view of the PC-98, the serial port is the cheapest way to send a couple of bytes to some external \u003ci\u003ething\u003c/i\u003e, and named pipes are one of many native ways for two Windows/Linux applications to efficiently communicate.\u003cbr\u003e\n\t\tThe only slight drawback of this approach is the expected high DOS memory requirement for rollback. Unless we find a way to \u003ci\u003ereally\u003c/i\u003e compress game state snapshots to just a few KB, this approach will require a more modern DOS setup with EMS/XMS support instead of the pre-installed MS-DOS 3.30C on a certain widely circulated .HDI copy. But apart from that, all you'd need to do is run the separate netplay tool, pick the same pipe name in both the tool and the emulator, and you're good to go.\n\t\u003c/p\u003e\u003cfigure class=\"large\"\u003e\n\t\t\u003cimg src=\"/blog/static/2024-04-24-NP21W-COM-ports-over-named-pipes.png?4eb80d90\" alt=\"Screenshot of Neko Project 21/W's Serial option menu, with COM1 being configured to send over a named pipe\"\u003e\n\t\u003c/figure\u003e\u003cp\u003e\n\t\tIt could even work for real hardware, but would require the PC-98 to be linked to the separately running modern system via a null modem cable.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli id=\"win9x-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003eNative PC-98 Windows 9x netcode \u003cstrong\u003e(only for real PC-98 hardware equipped with an Ethernet card)\u003c/strong\u003e\u003c/i\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tEquivalent in features to 2), but pulls the netcode into the PC-98 system itself. The tool developed in 3) would then as a separate 32-bit or 16-bit Windows application that somehow communicates with the game running in a DOS window. The handful of real-hardware owners who have actually equipped their PC-98 with a network card such as the \u003ca href=\"https://simk98.github.io/np21w/docs/lgy98.html\"\u003eLGY-98\u003c/a\u003e would then no longer require the modern PC from 3) as a bridge in the middle.\u003cbr\u003e\n\t\tThis specific card also happens to be low-level-emulated by \u003ca href=\"https://simk98.github.io/np21w/\"\u003ethe 21/W fork of Neko Project\u003c/a\u003e. However, it makes little sense to use this technique in an emulator when compared to 3), as NP21/W requires a separately installed and configured TAP driver to actually be able to access your native Windows Internet connection. While \u003ca href=\"https://simk98.github.io/np21w/lan.html\"\u003ethe setup is well-documented\u003c/a\u003e and I did manage to get a working Internet connection inside an emulated Windows 95, it's \u003ca href=\"https://www.reddit.com/r/pc98/comments/1572sni/help_with_connecting_windows_95_to_the_internet/\"\u003edefinitely not foolproof\u003c/a\u003e. Not to mention DOSBox-X, which currently emulates the apparently hardware-compatible \u003ca href=\"https://dosbox-x.com/wiki/Guide%3ASetting-up-networking-in-DOSBox%E2%80%90X\"\u003eNE2000 card\u003c/a\u003e, but \u003ca href=\"https://github.com/joncampbell123/dosbox-x/blob/1652704994a74ac93582676a5c33248bad0c956f/src/hardware/ne2000.cpp#L1714\"\u003edisables its emulation in PC-98 mode\u003c/a\u003e, most likely because its I/O ports clash with the typical peripherals of a PC-98 system.\n\t\u003c/p\u003e\u003cp\u003e\n\t\tAnd that's not the end of the drawbacks:\n\t\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eNetplay would depend on the PC-98 versions of Windows 9x and its full network stack, nothing of which is required for the game itself.\u003c/li\u003e\n\t\t\u003cli\u003ePorting libdatachannel (and \u003ci\u003eespecially\u003c/i\u003e the required transport encryption) to Windows 95 will probably involve a bit of effort as well.\u003c/li\u003e\n\t\t\u003cli\u003eAs would actually finding a way to access \u003ca href=\"https://en.wikipedia.org/wiki/Virtual_8086_mode\"\u003eV86 mode\u003c/a\u003e memory from a 32-bit or 16-bit Windows process, particularly due to how isolated DOS processes are from the rest of the system and even each other. A quick investigation revealed three potential approaches:\u003cul\u003e\n\t\t\t\u003cli\u003eA 32-bit process could read the memory out of the address space of the console host process (\u003ccode\u003eWINOA32.MOD\u003c/code\u003e). There seems to be no way of locating the specific base address of a DOS process, but you could always do a brute-force search through the memory map.\u003c/li\u003e\n\t\t\t\u003cli\u003eIf started before Windows, TSRs will share their resident memory with both DOS and Win16 processes. The segment pointer would then be retrieved through a typical interrupt API.\u003c/li\u003e\n\t\t\t\u003cli\u003eWriting a VxD driver 😩\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\t\u003cli\u003eCorrectly setting up TH03 to run within Windows 95 to begin with can be rather tricky. The GDC clock speed check needs to be either patched out or overridden using \u003ca href=\"https://www.vector.co.jp/soft/dl/dos/hardware/se062761.html\"\u003emode-setting tools\u003c/a\u003e, Windows needs to be blocked from accessing the FM chip, and even then, \u003ccode\u003eMAIN.EXE\u003c/code\u003e might still immediately crash during the first frame and leave all of VRAM corrupted:\n\t\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\t\u003cimg src=\"/blog/static/2024-04-24-TH03-Windows-95-ingame-crash.png?255eca8e\" alt=\"Screenshot of the TH03 crash on a Windows 95 system emulated in Neko Project 21/W ver0.86 rev92β3\"\u003e\n\t\t\t\u003cfigcaption\u003eThis is probably a bug in the latest ver0.86 rev92β3 version of Neko Project 21/W; I got it to work fine on real hardware. \u003ca href=\"/blog/2019-11-06\"\u003e📝 StormySpace\u003c/a\u003e did run on the same emulated Windows 95 system without any issues, though. Regardless, it's still worth mentioning as a symbol of everything that can go wrong.\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\u003c/li\u003e\n\t\t\u003cli\u003eA matchmaking server would be much more of a requirement than in any of the emulator variants. Players are unlikely to run their favorite chat client on the same PC-98 system, and the signaling codes are way too unwieldy to type them in manually. (Then again, \u003ca href=\"https://xkcd.com/1782/\"\u003eIRC is always an option\u003c/a\u003e, and the people who would fund this variant are probably the exact same people who are already running IRC clients on their PC-98.)\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli id=\"dos-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003eNative PC-98 DOS netcode \u003cstrong\u003e(only for real PC-98 hardware equipped with an Ethernet card)\u003c/strong\u003e\u003c/i\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tConceptually the same as 4), but going yet another level deeper, replacing the Windows 9x network stack with \u003ca href=\"https://www.qsl.net/ja0rug/teene.html\"\u003ea DOS-based one\u003c/a\u003e. This might look even more intimidating and error-prone, but after I got \u003ccode\u003eping\u003c/code\u003e \u003ci\u003eand even Telnet\u003c/i\u003e working, I was pleasantly surprised at how much simpler it is when compared to the Windows variant. The whole stack consists of just one LGY-98 hardware information tool, a LGY-98 packet driver TSR, and a TSR that implements TCP/IP/UDP/DNS/ICMP and is configured with a plaintext file. I don't have any deep experience with these protocols, so I was quite surprised that you can implement all of them in a single 40\u0026nbsp;KiB binary. Installed as TSRs, the entire stack takes up an acceptable 82\u0026nbsp;KiB of conventional memory, leaving more than enough space for the game itself. And since both of the TSRs are open-source, we can even legally bundle them with the future modified game binaries.\u003cbr\u003e\n\t\tThe matchmaking issue from the Windows 9x approach remains though, along with the following issues:\n\t\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003ePorting libdatachannel and the required transport encryption to the TEEN stack seems even more time-consuming than a Windows 95 port.\u003c/li\u003e\n\t\t\u003cli\u003eThe TEEN stack has no UI for specifying the system's or gateway's IP addresses outside of its plaintext configuration file. This provides a nice opportunity for adding a new \u003ci\u003eInternet settings\u003c/i\u003e menu with great error feedback to the game itself. Great for UX, but it's another thing I'd have to write.\u003c/li\u003e\n\t\t\u003cli\u003eThe LGY-98 is not the only network card for the PC-98. Others might have more complicated DOS drivers that might not work as seamlessly with the TEEN stack, or have no preserved DOS drivers at all. Heck, the most time-consuming part of the DOS setup was \u003ca href=\"https://www.buffalo.jp/support/download/detail/?dl_contents_id=60432\"\u003efinding the correct download link for the LGY-98 packet driver\u003c/a\u003e, as \u003ca href=\"http://product.buffalo.jp/download/driver/lan/lgy-98.html\"\u003ethe one link that appears in a lot of places\u003c/a\u003e only throws an \u003ci\u003eaccess denied\u003c/i\u003e error these days. \u003cstrong\u003eEdit (2024-04-30):\u003c/strong\u003e \u003ca href=\"https://lainnet.arcesia.net/repo/LGYTEEN.ZIP\"\u003espaztron64 is now hosting both the LGY-98 packet driver and the entire TEEN bundle on his homepage.\u003c/a\u003e\u003cbr\u003e\n\t\tIf you're interested in funding this variant and \u003ci\u003eare\u003c/i\u003e using a non-LGY-98 card on real hardware, make sure you get general Internet working on DOS first.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli id=\"port-2024-04-24\"\u003e\u003cp\u003e\n\t\t\u003ci\u003ePorting the game first\u003c/i\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tAs always, this is the premium option. If the entire game already runs as a standalone executable on a modern system, we can just put all the netcode into the same binary and have the most seamless integration possible.\n\t\u003c/p\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThat leaves us with these prerequisites:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e1), by definition, needs nothing from ReC98, and I could theoretically start implementing it right now. If you're interested in funding it, just tell me via the usual Twitter or Discord channels.\u003c/li\u003e\n\t\u003cli\u003e2) through 5) require at least 100% RE of TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e to facilitate the new menu code. Reverse-engineering all rendering-related code in \u003ccode\u003eMAIN.EXE\u003c/code\u003e would be nice for performance, but we don't strictly need \u003ci\u003eall\u003c/i\u003e of it before we start. Re-simulated frames can just skip over the few pieces of rendering code we \u003ci\u003edo\u003c/i\u003e know, and we can gradually increase the skipped area of code in future pushes. 100% PI won't be a requirement either, as I expect the \u003ccode\u003eMAIN.EXE\u003c/code\u003e part of the interfacing netcode layer to be thin enough that it can easily fit within the original game's code layout.\u003cbr\u003e\n\tTherefore, funding TH03 \u003ccode\u003eOP.EXE\u003c/code\u003e RE is the clearest way you can signal to me that you want netplay with nice UX.\u003c/li\u003e\n\t\u003cli\u003e6), obviously, requires all of TH03 to be RE'd, decompiled, cleaned up, and ported to modern systems. Currently, TH03 appears to be the second-easiest game to port behind TH02:\u003cul\u003e\n\t\t\u003cli\u003eAlthough TH03 already has more needlessly micro-optimized ASM code than TH02 and there's even more to come, it still appears to have way less than TH04 or TH05.\u003c/li\u003e\n\t\t\u003cli\u003eIts game logic and rendering code seem to be somewhat neatly separated from each other, unlike TH01 which deeply intertwines them.\u003c/li\u003e\n\t\t\u003cli\u003eIts graphics seem free of obvious bugs, unlike – again — the flicker-fest that is TH01.\u003c/li\u003e\n\t\u003c/ul\u003eBut still, it's the game with the least amount of RE%. Decompilation \u003ci\u003emight\u003c/i\u003e get easier once I've worked myself up to the higher levels of game code, and even more so if we're lucky and all of the 9 characters are coded in a similar way, but I can't promise anything at this point.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOnce we've reached any of these prerequisites, I'll set up a separate \u003ci\u003ecampaign\u003c/i\u003e funding method that runs parallel to the cap. As netplay is one of those big features where incremental progress makes little sense \u003ci\u003eand\u003c/i\u003e we can expect wide community support for the idea, I'll go for a more classic crowdfunding model with a fixed goal for the minimum feature set and stretch goals for optional quality-of-life features. Since I've still got two other big projects waiting to be finished, I'd like to at least complete the Shuusou Gyoku Linux port before I start working on TH03 netplay, even if we manage to hit any of the funding goals before that.\n\u003c/p\u003e\u003chr id=\"hitcirc-2024-04-24\"\u003e\u003cp\u003e\n\tFor the first time in a long while, the actual content of this push can be listed fairly quickly. I've now RE'd:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003econversions from playfield-relative coordinates to screen coordinates and back (a first in PC-98 Touhou; even TH02 uses screen space for every coordinate I've seen so far),\u003c/li\u003e\n\t\u003cli\u003ethe low-level code that moves the player entity across the screen,\u003c/li\u003e\n\t\u003cli\u003ea copy of the per-round frame counter that, for some reason, resets to 0 at the start of the \u003ci\u003eWin/Lose\u003c/i\u003e animation, resetting a bunch of animations with it,\u003c/li\u003e\n\t\u003cli\u003ea global hitbox with one variable that sometimes stores the center of an entity, and sometimes its top-left corner, \u003c/li\u003e\n\t\u003cli\u003eand the 48×48 hit circles from \u003ccode\u003eEN2.PI\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIt's also the third TH03 \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e push in a row that features inappropriate ASM code in places that really, really didn't need any. As usual, the code is worse than what Turbo C++ 4.0J would generate for idiomatic C code, and the surrounding code remains full of untapped and quick optimization opportunities anyway. This time, the biggest joke is the sprite offset calculation in the hit circle rendering code:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e_BX = (circle-\u003eage - 1);\n_BX \u003e\u003e= 2;\n_BX *= 2;\nuint16_t sprite_offset_in_sprite16_area = (0x1910 + _BX + _BX + _BX);\u003c/pre\u003e\n\t\u003cfigcaption\u003e\n\t\tA multiplication with 6 would have compiled into a single \u003ccode\u003eIMUL\u003c/code\u003e instruction. This compiles into 4 \u003ccode\u003eMOVs\u003c/code\u003e, one \u003ccode\u003eIMUL\u003c/code\u003e (with 2), and two \u003ccode\u003eADD\u003c/code\u003es. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This surely must have been left in on purpose for us to laugh about it one day?\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut while we've all come to expect the usual share of ZUN bloat by now, this is also the first push \u003ci\u003ewithout\u003c/i\u003e either a ZUN bug or a landmine since I started using these terms! 🎉 It does contain a single ZUN \u003ci\u003equirk\u003c/i\u003e though, which can also be found in the hit circles. This animation comes in two types with different caps: 12 animation slots across both playfields for the \u003ci\u003eenemy\u003c/i\u003e circles shown in alternating bright/dark yellow colors, whereas the white animation for the player characters has a cap of… 1? P2 takes precedence over P1 because its update code always runs last, which explains what happens when both players get hit within the 16 frames of the animation:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tIf they both get hit on the exact same frame, the animation for P1 never plays, as P2 takes precedence.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tIf the other player gets hit within 16 frames of an active white circle animation, the animation is reinitialized for the other player as there's only a single slot to hold it. Is this supposed to telegraph that the other player got hit without them having to look over to the other playfield? After all, they're drawn on top of most other entities, but below the player. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-24-TH03-player-hit-circles-same.webp?fd3377dc\" preload=\"none\" controls data-title=\"Both players get hit on the same frame\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-04-24-TH03-player-hit-circles-same.avi?104fe5d4\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-24-TH03-player-hit-circles-same.webm?fc61bcb2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-24-TH03-player-hit-circles-same.webm?1c943a8c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-24-TH03-player-hit-circles-same.webm?32579c61\" type=\"video/webm\"\u003eVideo of TH03's white player hit circle animation, showing off how P2 takes precedence when both players get hit on the same frame. \u003ca href=\"/blog/static/video/zmbv/2024-04-24-TH03-player-hit-circles-same.avi?104fe5d4\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"87\" data-title=\"Hit\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-24-TH03-player-hit-circles-displaced.webp?691f2229\" preload=\"none\" controls data-title=\"Other player gets hit during circle animation\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-04-24-TH03-player-hit-circles-displaced.avi?8c4f74be\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-24-TH03-player-hit-circles-displaced.webm?988ad467\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-24-TH03-player-hit-circles-displaced.webm?022f6341\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-24-TH03-player-hit-circles-displaced.webm?74952c99\" type=\"video/webm\"\u003eVideo of TH03's white player hit circle animation, showing off how the single animation slot causes this animation to get canceled if the other player gets hit within its 16 frames. \u003ca href=\"/blog/static/video/zmbv/2024-04-24-TH03-player-hit-circles-displaced.avi?8c4f74be\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"79\" data-title=\"P1 gets hit\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"87\" data-title=\"P2 gets hit\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSPRITE16 uses the PC-98's EGC to draw these single-color sprites. If the EGC is already set up, it can be set into a GRCG-equivalent RMW mode using the pattern/read plane register (\u003ccode\u003e0x4A2\u003c/code\u003e) and foreground color register (\u003ccode\u003e0x4A6\u003c/code\u003e), together with setting the mode register (\u003ccode\u003e0x4A4\u003c/code\u003e) to \u003ccode\u003e0x0CAC\u003c/code\u003e. Unlike the typical blitting operations that involve its 16-dot pattern register, the EGC even supports 8- or 32-bit writes in this mode, just like the GRCG. \u003ca href=\"/blog/2023-03-05#egc-2023-03-05\"\u003e📝 As expected\u003c/a\u003e for EGC features beyond the most ordinary ones though, T98-Next simply sets every written pixel to black on a 32-bit write. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Comparing the actual performance of such writes to the GRCG would be \u003ca href=\"/blog/2024-02-03\"\u003e📝 yet another\u003c/a\u003e interesting question to benchmark.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: I think it's time for ReC98's build system to reach its final form.\n\tFor almost 5 years, I've been using \u003ca href=\"https://activitypub.nmlgc.net/@rec98/statuses/01DJE866685JA15FXGR9FWP02M\"\u003ean unreleased sane build system\u003c/a\u003e on a parallel private branch that was \u003ci\u003ejust\u003c/i\u003e missing some final polish and bugfixes. Meanwhile, the public repo is still using the project's initial Makefile that, \u003ca href=\"/blog/2020-09-03\"\u003e📝 as typical for Makefiles\u003c/a\u003e, is so unreliable that \u003ccode\u003eBUILD16B.BAT\u003c/code\u003e force-rebuilds everything by default anyway. While my build system has scaled decently over the years, something even better happened in the meantime: \u003ca href=\"http://takeda-toshiya.my.coocan.jp/msdos/index.html\"\u003eMS-DOS Player\u003c/a\u003e, a DOS emulator exclusively meant for seamless integration of CLI programs into the Windows console, has been \u003ca href=\"https://github.com/cracyc/msdos-player\"\u003eforked\u003c/a\u003e and enhanced enough to finally run Turbo C++ 4.0J at an acceptable speed. So let's remove DOSBox from the equation, merge the 32-bit and 16-bit build steps into a single 32-bit one, set all of this up in a user-friendly way, and maybe squeeze even more performance out of MS-DOS Player specifically for this use case.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-07-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-04-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-04-24T13:14:41Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-04-11",
      "url": "https://rec98.nmlgc.net/blog/2024-04-11",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-04-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-03-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-04-11\"\u003e\u003ctime datetime=\"2024-04-11T22:45:29Z\"\u003e2024-04-11 22:45\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0278\"\u003eP0278\u003c/a\u003e\n\t\t\tTH02 decompilation (Endings)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b6a7285...f0fbaf6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0279\"\u003eP0279\u003c/a\u003e\n\t\t\tTH02 decompilation (Staff Roll + Verdict screen + Stage bonus tables)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f0fbaf6...20bac82\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sigma\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH02\u0026#39;s Extra Stage boss.\"\u003esigma\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\u003cstyle\u003e\n\t.colors-2024-04-11 img {\n\t\twidth: 640px;\n\t}\n\n\t.numbers.skill-2024-04-11 td {\n\t\ttext-align: left;\n\t}\n\n\t.numbers.skill-2024-04-11 tbody th {\n\t\tfont-weight: normal;\n\t}\n\n\t.numbers.bonus-2024-04-11 th {\n\t\ttext-align: left;\n\t\tfont-weight: normal;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tThat was quick: In a surprising turn of events, Romantique Tp themselves came in just one day after the last blog post went up, \u003ca href=\"https://twitter.com/Romantique_Tp/status/1766898006329053256\"\u003eupdated me with their current and much more positive opinion on Sound Canvas VA\u003c/a\u003e, and \u003ca href=\"https://twitter.com/Romantique_Tp/status/1766895996645056902\"\u003econfirmed that real SC-88Pro hardware clamps invalid Reverb Macro values to the specified range\u003c/a\u003e. I promised to release a new Sound Canvas VA BGM pack for free once I knew the exact behavior of real hardware, so let's go right back to Seihou and also integrate the necessary SysEx patches into the game's MIDI player behind a toggle. This would also be a great occasion to quickly incorporate some long overdue code maintenance and build system improvements, and a migration to \u003ca href=\"https://en.cppreference.com/w/cpp/language/modules\"\u003eC++ modules\u003c/a\u003e in particular. When I started the Shuusou Gyoku Linux port a year ago, the combination of modules and \u003ccode\u003e\u0026lt;windows.h\u0026gt;\u003c/code\u003e threw lots of weird errors and even crashed the Visual Studio compiler. But nowadays, \u003ca href=\"https://devblogs.microsoft.com/cppblog/integrating-c-header-units-into-office-using-msvc-2-n/\"\u003eMicrosoft even uses modules in the Office code base\u003c/a\u003e. This \u003ci\u003emust\u003c/i\u003e mean that these issues are fixed by now, right?\u003cbr\u003e\n\tWell, there's \u003ci\u003estill\u003c/i\u003e a bug that causes the modularized C++ standard library to be basically unusable in combination with the static analyzer, \u003ca href=\"https://developercommunity.visualstudio.com/t/Module-compilation-with-analyze-ignores/10627451\"\u003eand somehow, I was the first one to report it\u003c/a\u003e. So it's 3½ years after C++20 was finalized, and somehow, modules are still a bleeding-edge feature and a second-class citizen in even \u003ca href=\"https://en.cppreference.com/w/Template:cpp/compiler_support/20\"\u003ethe compiler that supports them the best\u003c/a\u003e. I want fast compile times already! 😕\u003cbr\u003e\n\tThankfully, Microsoft \u003ca href=\"https://developercommunity.visualstudio.com/t/Module-compilation-with-analyze-ignores/10627451#T-N10628920\"\u003eagrees that this is a bug, and will work on it at some point\u003c/a\u003e. While we're waiting, let's return to the original plan of decompiling the endings of the one PC-98 Touhou game that still needed them decompiled.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#endings-2024-04-11\"\u003eTH02's endings\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#staffroll-2024-04-11\"\u003eTH02's Staff Roll\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#verdict-2024-04-11\"\u003eTH02's verdict screen, and its hidden challenge\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#bonus-2024-04-11\"\u003eTH02's end-of-stage bonus screens\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"endings-2024-04-11\"\u003e\u003cp\u003e\n\tAfter the textless slideshows of TH01, TH02 was the first Touhou game to feature lore text in its endings. Given that this game stores its \u003ca href=\"/blog/2023-11-01#th02-2023-11-01\"\u003e📝 in-game dialog text\u003c/a\u003e in fixed-size plaintext files, you wouldn't expect anything more fancy for the endings either, so it's not surprising to see that the \u003ccode\u003eEND?.TXT\u003c/code\u003e files use the same concept, with 44 visible bytes per line followed by two bytes of padding for the CR/LF newline sequence. Each of these lines is typed to the screen in full, with all whitespace and a fixed time for each 2-byte chunk.\u003cbr\u003e\n\tAs a result, everything surrounding the text is just as hardcoded as TH01's endings were, which once again opens up the possibility of freely integrating all sorts of creative animations without the overhead of an interpreter. Sadly, TH02 only makes use of this freedom in a mere two cases: the picture scrolling effect from Reimu's head to Marisa's head in the Bad Endings, and a single hardware palette change in the Good Endings.\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-11-TH02-Bad-Endings-scroll.webp?2327a38f\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"100\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-04-11-TH02-Bad-Endings-scroll.avi?6a8ca507\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-11-TH02-Bad-Endings-scroll.webm?243b9f66\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-11-TH02-Bad-Endings-scroll.webm?c4cf1aa2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-11-TH02-Bad-Endings-scroll.webm?e35dbcc2\" type=\"video/webm\"\u003eVideo of the scrolling effect from Reimu's head to Marisa's head seen in TH02's Bad Endings. \u003ca href=\"/blog/static/video/zmbv/2024-04-11-TH02-Bad-Endings-scroll.avi?6a8ca507\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003ePowered by master.lib's \u003ccode\u003eegc_shift_down()\u003c/code\u003e.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-04-11-TH02-Good-Endings-line-13.png?c3d45241\"\n\t\t\tdata-title=\"Line 13\"\n\t\t\talt=\"Screenshot of the (0-based) line #13 in TH02's Good Endings, together with its associated (and colored) picture\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-04-11-TH02-Good-Endings-line-14.png?9cef0ac4\"\n\t\t\tdata-title=\"Line 14\"\n\t\t\talt=\"Screenshot of the (0-based) line #14 in TH02's Good Endings, showing off how it doesn't change the picture of the previous line and only applies a different grayscale palette\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tSame image, different palette. Note how the palette for 2️⃣ must still contain a green color for the VRAM-rendered bold text, which the image is not supposed to use.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHardcoding also still made sense for this game because of how the ending text is structured. The Good and Bad Endings for the individual shot types respectively share 55% and 77% of their text, and both only diverge after the first 27 lines. In straight-line procedural code, this translates to one branch for each shot type at a single point, neatly matching the high-level structure of these endings.\n\u003c/p\u003e\u003cp\u003e\n\tBut that's the end of the positive or neutral aspects I can find in these scripts. The worst part, by far, is ZUN's approach to displaying the text in alternating colors, and how it impacts the entire structure of the code.\u003cbr\u003e\n\tThe simplest solution would have involved a hardcoded array with the color of each line, \u003ca href=\"https://github.com/nmlgc/ReC98/blob/ae2fc2865a74b095bdbc8b469073f43c8cbc2d98/th02_main.asm#L29994-L30170\"\u003ejust like how the in-game dialogs store the face IDs for each text box\u003c/a\u003e. But for whatever reason, ZUN did not apply this piece of wisdom to the endings and instead hardcoded these color changes by… mutating a global variable before calling the text typing function for every individual line.\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This approach ruins any possibility of compressing the script code into loops. While ZUN \u003ci\u003edid\u003c/i\u003e use loops, all of them are very short because they can only last until the next color change. In the end, the code contains \u003ci\u003e90\u003c/i\u003e explicitly spelled-out calls to the 5-parameter line typing function that only vary in the pointer to each line and in the slower speed used for the one or two final lines of each ending. As usual, I've \u003ca href=\"TODO\"\u003ededuplicated the code in the ReC98 repository down to a sensible level\u003c/a\u003e, but here's the full inlined and macro-expanded horror:\n\u003c/p\u003e\u003cfigure\u003e\u003cdiv\u003e\u003ca\n\thref=\"/blog/static/2024-04-11-TH02-Bad-Endings-expanded.png?372249ad\"\u003e\u003cimg\n\tsrc=\"/blog/static/2024-04-11-TH02-Bad-Endings-expanded.png?372249ad\" alt=\"Raw decompilation of TH02's script function for its three Bad Endings, without inline function or macro trickery\"\u003e\u003c/a\u003e\u003ca\n\thref=\"/blog/static/2024-04-11-TH02-Good-Endings-expanded.png?ec935fe2\"\u003e\u003cimg\n\tsrc=\"/blog/static/2024-04-11-TH02-Good-Endings-expanded.png?ec935fe2\" alt=\"Raw decompilation of TH02's script function for its three Good Endings, without inline function or macro trickery\"\u003e\u003c/a\u003e\u003c/div\u003e\u003cfigcaption\u003e\n\tIt's highly likely that this is what ZUN hacked into his PC-98 and was staring at back in 1997. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAll this redundancy bloats the two script functions for the 6 endings to a whopping 3,344 bytes inside TH02's \u003ccode\u003eMAINE.EXE\u003c/code\u003e. In particular, the single function that covers the three Good Endings ends up with a total of 631 x86 ASM instructions, making it the single largest function in TH02 and the \u003cspan class=\"hovertext\" title=\"The other 6 functions are all part of TH01.\"\u003e7\u003csup\u003eth\u003c/sup\u003e longest function\u003c/span\u003e in all of PC-98 Touhou. If the \u003ca href=\"/blog/2022-03-05\"\u003e📝 single-executable build\u003c/a\u003e for TH02's \u003ccode\u003edebloated\u003c/code\u003e and \u003ccode\u003eanniversary\u003c/code\u003e branches ends up needing a few more KB to reduce its size below the original \u003ccode\u003eMAIN.EXE\u003c/code\u003e, there are lots of opportunities to compress it all.\n\u003c/p\u003e\u003cp\u003e\n\tThe ending text can also be fast-forwarded by holding any key. As we've come to expect for this sort of ZUN code, the text typing function runs its own rendering loop with VSync delays and input detection, which means that we \u003ca href=\"/blog/2023-11-01#ref-2023-11-01\"\u003e📝 once\u003c/a\u003e \u003ca href=\"/blog/2023-11-01#ref-2023-11-01\"\u003e📝 again\u003c/a\u003e have to talk about the infamous quirk of the PC-98 keyboard controller in relation to held keys. We've still got 54 not yet decompiled calls to input detection functions left in this codebase, are you excited yet?! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tHolding any key speeds up the text of all ending lines before the last one by displaying two kana/kanji instead of one per rendered frame and reducing the delay between the rendered frames to \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e3\u003c/sub\u003e of its regular length. In pseudocode:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003efor(i = 0; i \u0026lt; number_of_2_byte_chunks_on_displayed_line; i++) {\n\tinput = convert_current_pc98_bios_input_state_to_game_specific_bitflags();\n\tadd_chunk_to_internal_text_buffer(i);\n\tblit_internal_text_buffer_from_the_beginning();\n\tif(input == INPUT_NONE) {\n\t\t// Basic case, no key pressed\n\t\tframe_delay(frames_per_chunk);\n\t} else if((i % 2) == 1) {\n\t\t// Key pressed, chunk number is odd.\n\t\tframe_delay(frames_per_chunk / 3);\n\t} else {\n\t\t// Key pressed, chunk number is even.\n\t\t// No delay; next iteration adds to the same frame.\n\t}\n}\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tThis is exactly the kind of code you would write if you wanted to deliberately maximize the impact of this hardware quirk. If the game happens to read the current input state right after a \u003ci\u003ekey up\u003c/i\u003e scancode for the last previously held and game-relevant key, it will then wrongly take the branch that uninterruptibly waits for the regular, non-divided amount of VSync interrupts. In my tests, this broke the rhythm of the fast-forwarded text about once per line. Note how this branch can also be taken on an even chunk: Rendering glyphs straight from font ROM to VRAM is not exactly cheap, and if each iteration (needlessly) blits one more full-width glyph than the last one, the probability of a \u003ci\u003ekey up\u003c/i\u003e scancode arriving in the middle of a frame only increases.\u003cbr\u003e\n\tThe fact that TH02 allows \u003ci\u003eany\u003c/i\u003e of the supported input keys to be held points to another detail of this quirk I haven't mentioned so far. If you press multiple keys at once, the PC-98's keyboard controller only sends the periodic \u003ci\u003ekey up\u003c/i\u003e scancodes as long as you are holding the \u003ci\u003elast\u003c/i\u003e key you pressed. Because the controller only remembers this last key, pressing and releasing any other key would get rid of these scancodes for all keys you are still holding.\u003cbr\u003e\n\tAs usual, this ZUN bug only occurs on real hardware and with DOSBox-X's correct emulation of the PC-98 keyboard controller.\n\u003c/p\u003e\u003chr id=\"staffroll-2024-04-11\"\u003e\u003cp\u003e\n\tAfter the ending, we get to witness the most seamless transition between ending and Staff Roll in any Touhou game as the BGM immediately changes to the Staff Roll theme, and the ending picture is shifted into the same place where the Staff Roll pictures will appear. Except that the code misses the exact position by four pixels, and cuts off another four pixels at the right edge of the picture:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.webp?85455849\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"556\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.avi?deba61d3\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.webm?8899b1f5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.webm?858aed14\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.webm?0994384e\" type=\"video/webm\"\u003eVideo of TH02's transition animation from the Reimu-B Good Ending to the Staff Roll, drawing attention to its two off-by-four errors and the green 1-pixel line at the right side of the final Ending image. \u003ca href=\"/blog/static/video/zmbv/2024-04-11-TH02-Good-Ending-2-to-Staff-Roll.avi?deba61d3\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"462\" data-title=\"Shift animation starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eAlso, note the green 1-pixel line at the right edge of this specific picture. This is a bug in the .PI file where the picture is indeed shifted one pixel to the left. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhat follows is a comparatively large amount of \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e content for a single scene. It starts right at the end of this underappreciated 11-frame animation loaded from \u003ccode\u003eENDFT.BFT\u003c/code\u003e:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 320px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-04-11-TH02-ENDFT.BFT.gif?a28d3173\" alt=\"TH02's ENDFT.BFT\" style=\"width: 320px\"\u003e\n\t\u003cfigcaption\u003e\n\t\tWastefully using the 4bpp BFNT format. The single \u003cimg class=\"inline_sprite\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAgAQAAAACtSMLBAAAAWUlEQVR42mOgE6j/8cHmgw2YmWCQIBEhAWEmJEjkQJgMB1CYaRAmY0OCRDKEycwAZ7IBmYkzwEweIDPhBpRpYABlSkgAmRFgpoGEzIMEC6gb2Bs+SDDQFQAA718X4R8AtXYAAAAASUVORK5CYII=\" width=\"80\" height=\"32\" alt=\"ZUN\"\u003e frame at the end of the animation is \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e; while it might look identical to the \u003cspan lang=\"ja\"\u003eＺＵＮ\u003c/span\u003e glyphs later on in the Staff Roll, that's only because both are independently rendered boldfaced versions of the same font ROM glyphs. Then again, it does prove that ZUN created this animation on a PC-98 model made by NEC, as the \u003ca href=\"https://en.wikipedia.org/w/index.php?title=PC-98\u0026oldid=1209823165#Epson_clones\"\u003eEpson clones\u003c/a\u003e used a font ROM with a distinctly different look.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTH02's Staff Roll is also unique for the pre-made screenshots of all 5 stages that get shown together with a fancy rotating rectangle animation while the Staff Roll progresses in sync with the BGM. The first interesting detail shows up immediately after the first image, where the code jumps over one of the 320×200 quarters in \u003ccode\u003eED06.PI\u003c/code\u003e, leaving the screenshot of the Stage 2 midboss \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e.\u003cbr\u003e\n\tAll of the cutscenes in PC-98 Touhou store their pictures as 320×200 quarters within a single 640×400 .PI file. Anywhere else, all four quarters are supposed to be displayed with the same palette specified in the .PI header, but TH02's Staff Roll screenshots are also unique in how all quarters beyond the top-left one require palettes loaded from external .RGB files to look right. Consequently, the game doesn't clearly specify the intended palette of this unused screenshot, and leaves two possibilities:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThe unused second 320×200 quarter of TH02's \u003ccode\u003eED06.PI\u003c/code\u003e, displayed in the Stage 2 color palette used in-game.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe unused second 320×200 quarter of TH02's \u003ccode\u003eED06.PI\u003c/code\u003e, displayed in the palette specified in the .PI header. These are the colors you'd see when looking at the file in a .PI viewer, when converting it into another format with the usual tools, or \u003ca href=\"https://www.spriters-resource.com/nec_pc_9801/touhoufuumarokuthestoryofeasternwonderland/sheet/103862/\"\u003ein sprite rips that don't take TH02's hardcoded palette changes into account\u003c/a\u003e. These colors are only intended for the Stage 1 screenshot in the top-left quarter of the file.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe unused second 320×200 quarter of TH02's \u003ccode\u003eED06.PI\u003c/code\u003e, displayed in the palette from \u003ccode\u003eED06B.RGB\u003c/code\u003e, which the game uses for the following screenshot of the Meira fight. As it's from the same stage, it \u003ci\u003ealmost\u003c/i\u003e matches the in-game colors seen in 1️⃣, and only differs in the white color (\u003ccode style=\"background-color: #fff\"\u003e#FFF\u003c/code\u003e) being slightly red-tinted (\u003ccode style=\"background-color: #fcc\"\u003e#FCC\u003c/code\u003e).\n\t\u003c/div\u003e\u003c/figcaption\u003e\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED06.PI-1-Stage-2-palette.png?8061184a\" data-title=\"Stage 2 palette\" alt=\"\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED06.PI-1-PI-palette.png?35d7ca22\" data-title=\"\u003ccode\u003eED06.PI\u003c/code\u003e palette\" alt=\"\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED06.PI-1-ED06B.RGB-palette.png?efbb31f7\"\n\t\tdata-title=\"\u003ccode\u003eED06B.RGB\u003c/code\u003e palette\"\n\t\talt=\"\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIt might seem obvious that the Stage 2 palette in 1️⃣ is the correct one, but ZUN indeed uses \u003ccode\u003eED06B.RGB\u003c/code\u003e with the red-tinted white color for the following screenshot of the Meira fight. Not only does this palette not match Meira's in-game appearance, but it also discolors the rectangle animation and the surrounding Staff Roll text:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-11-TH02-Staff-Roll-ED06.PI-2.webp?e8cfd2a0\" preload=\"none\" controls width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"26\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-04-11-TH02-Staff-Roll-ED06.PI-2.avi?0ccbd80b\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-11-TH02-Staff-Roll-ED06.PI-2.webm?c711277c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-11-TH02-Staff-Roll-ED06.PI-2.webm?81998dd5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-11-TH02-Staff-Roll-ED06.PI-2.webm?64bbb29b\" type=\"video/webm\"\u003eVideo of the transition from picture #2 to #3 in TH02's Staff Roll, demonstrating the palette change with the unfitting red-tinted white color. \u003ca href=\"/blog/static/video/zmbv/2024-04-11-TH02-Staff-Roll-ED06.PI-2.avi?0ccbd80b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eAlso, that tearing on frame #1 is not a recording artifact, but the expected result of yet another VSync-related landmine. 💣 This time, it's caused by the combination of 1) the entire sequence from the ending to the verdict screen being single-buffered, and 2) this animation always running \u003ci\u003eimmediately\u003c/i\u003e after an expensive operation (640×400 .PI image loading and blitting to VRAM, 320×200 VRAM inter-page copy, or hardware palette loading from a packed file), without waiting for the VSync interrupt. This makes it highly likely for the first frame of this animation to start rendering at a point where the (real or emulated) electron beam has already traveled over a significant portion of the screen.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut when I went into Stage 2 to compare these colors to the in-game palette, I found something even more curious. ZUN obviously made this screenshot with the Reimu-C shot type, but one of the shot sprites looks slightly different from how it does in-game. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e These screenshots must have been made earlier in development when the sprite didn't yet feature the second ring at the top. The same applies to the Stage 4 screenshot later on:\n\u003c/p\u003e\u003cfigure class=\"colors-2024-04-11 pixelated\" style=\"line-height: 0;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cdiv data-title=\"Staff Roll screenshots\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED06.PI-2.png?2971b95f\"\n\t\talt=\"Original version of the third 320×200 quarter from TH02's ED06.PI, representing the Meira boss fight and showing off an old version of the Reimu-C shot sprites\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED07.PI-1.png?7afb7abc\"\n\t\talt=\"Original version of the first 320×200 quarter from TH02's ED07.PI, representing Stage 4 and showing off an old version of the Reimu-C shot sprites\"\n\t\u003e\u003c/div\u003e\u003cdiv data-title=\"Final sprites and colors\" class=\"active\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED06.PI-2-final-shots.png?14539cb6\"\n\t\talt=\"Edited version of the third 320×200 quarter from TH02's ED06.PI, representing the Meira boss fight; Reimu-C's shot sprites were replaced with their final version\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-04-11-TH02-ED07.PI-1-final-shots.png?f638f395\"\n\t\talt=\"Edited version of the first 320×200 quarter from TH02's ED07.PI, representing Stage 4; Reimu-C's shot sprites were replaced with their final version\"\n\t\u003e\u003c/div\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFinally, the rotating rectangle animation delivers one more minor rendering bug. Each of the 20 frames removes the largest and outermost rectangle from VRAM by redrawing it in the same black color of the background before drawing the remaining rectangles on top. The corners of these rectangles are placed on a shrinking circle that starts with a radius of 256 pixels and is centered at (﻿192,\u0026nbsp;200﻿), which results in a maximum possible X coordinate of 448 for the rightmost corner of the rectangle. However, the Staff Roll text starts at an X coordinate of 416, causing the first two full-width glyphs to still fall within the area of the circle. Each line of text is also only rendered once before the animation. So if any of the rectangles then happens to be placed at an angle that causes its edges to overlap the text, its removal will cut small holes of black pixels into the glyphs:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-04-11-TH02-Staff-Roll-text-holes.webp?4a9fcf82\" preload=\"none\" controls width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"522\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2024-04-11-TH02-Staff-Roll-text-holes.avi?b7beb718\"\u003e\u003csource src=\"/blog/static/video/av1/2024-04-11-TH02-Staff-Roll-text-holes.webm?c2bb951d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-04-11-TH02-Staff-Roll-text-holes.webm?dfeb37be\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-04-11-TH02-Staff-Roll-text-holes.webm?2d96ae4e\" type=\"video/webm\"\u003eVideo of the last 5 screens in TH02's Staff Roll and their rotating rectangle animations, showing off how the original way of unblitting can cut holes into the text. \u003ca href=\"/blog/static/video/zmbv/2024-04-11-TH02-Staff-Roll-text-holes.avi?b7beb718\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"7\" data-title=\"First hole\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"132\" data-title=\"Second hole\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"373\" data-title=\"ＴＥＳＴ　ＰＬＡＹＥＲ\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThe green dotted circle corresponds to the newest/smallest rectangle. Note how ZUN only happened to avoid the holes for the two final animations by choosing an initial angle and angular velocity that causes the resulting rectangles to just barely avoid touching the \u003cspan lang=\"ja\"\u003eＴＥＳＴ　ＰＬＡＹＥＲ\u003c/span\u003e glyphs.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"verdict-2024-04-11\"\u003e\u003cp\u003e\n\tAt least the following verdict screen manages to have no bugs aside from the slightly imperfect centering of its table values, and only comes with a small amount of additional bloat. Let's get right to the mapping from skill points to the 12 title strings from \u003ccode\u003eEND3.TXT\u003c/code\u003e, because one of them is not like the others:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers skill-2024-04-11\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\u003cth\u003eSkill\u003c/th\u003e\u003cth style=\"text-align: left;\"\u003eTitle\u003c/th\u003e\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\u003cth\u003e≥100\u003c/th\u003e\u003ctd lang=\"ja\"\u003e神を超えた巫女！！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e90 - 99\u003c/th\u003e\u003ctd lang=\"ja\"\u003eもはや神の領域！！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e80 - 99\u003c/th\u003e\u003ctd lang=\"ja\"\u003eＡ級シューター！！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e78 - 79\u003c/th\u003e\u003ctd lang=\"ja\"\u003eうきうきゲーマー！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e\u003cb\u003e77\u003c/b\u003e\u003c/th\u003e\u003ctd lang=\"ja\"\u003e\u003cb\u003eバニラはーもにー！\u003c/b\u003e\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e70 - 76\u003c/th\u003e\u003ctd lang=\"ja\"\u003eうきうきゲーマー！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e60 - 69\u003c/th\u003e\u003ctd lang=\"ja\"\u003eどきどきゲーマー！\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e50 - 59\u003c/th\u003e\u003ctd lang=\"ja\"\u003e要練習ゲーマー\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e40 - 49\u003c/th\u003e\u003ctd lang=\"ja\"\u003e非ゲーマー級\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e30 - 39\u003c/th\u003e\u003ctd lang=\"ja\"\u003eちょっとだめ\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e20 - 29\u003c/th\u003e\u003ctd lang=\"ja\"\u003e非人間級\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e10 - 19\u003c/th\u003e\u003ctd lang=\"ja\"\u003e人間でない何か\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003cth\u003e≤9\u003c/th\u003e\u003ctd lang=\"ja\"\u003e死んでいいよ、いやいやまじで\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tLooks like I'm the first one to document the required skill points as well? \u003ca href=\"https://en.touhouwiki.net/index.php?title=Story_of_Eastern_Wonderland/Translation\u0026oldid=465792#End-of-Game_Player_Ratings\"\u003eEveryone\u003c/a\u003e \u003ca href=\"https://seesaawiki.jp/toho-motoneta_2nd/d/%c5%ec%ca%fd%c9%f5%cb%e2%cf%bf#content_block_37\"\u003eelse\u003c/a\u003e just copy-pastes \u003ccode\u003eEND3.TXT\u003c/code\u003e without providing context.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tSo how would you get exactly 77 and achieve \u003ca href=\"https://www.youtube.com/clip/Ugkxdl4YKalsywHCrS0stLkG7fJCWR2oG-32\"\u003evanilla harmony\u003c/a\u003e? Here's the formula:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\"\u003e\u003ctr\u003e\n\t\u003ctd\u003e\u003c/td\u003e\u003ctd\u003eDifficulty level*\u003ccode\u003e × 20\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003ctd\u003e+\u003c/td\u003e\u003ctd\u003e\u003ccode\u003e10 - (\u003c/code\u003eContinues used\u003ccode\u003e × 3)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003ctd\u003e+\u003c/td\u003e\u003ctd\u003e\u003ccode\u003emax((50 - (\u003c/code\u003eLives lost\u003csup\u003e†\u003c/sup\u003e\u003ccode\u003e × 3) - \u003c/code\u003eBombs used\u003csup\u003e†\u003c/sup\u003e\u003ccode\u003e), 0)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003ctd\u003e+\u003c/td\u003e\u003ctd\u003e\u003ccode\u003emin(max(\u003ca href=\"/blog/2023-06-07\"\u003e📝 item_skill\u003c/a\u003e, 0), 25)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\t* Ranges from 0 (Easy) to 3 (Lunatic).\u003cbr\u003e\n\t\u003csup\u003e†\u003c/sup\u003e Across all 5 stages.\u003cbr\u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWith Easy Mode capping out at \u003cspan class=\"hovertext\" title=\"Perfect run with no continues (+10), lives or bombs lost (+50), and a maximum item_skill of (+25)\"\u003e85\u003c/span\u003e, this is possible on every difficulty, although it requires increasingly perfect play the lower you go. Reaching 77 on purpose, however, pretty much demands a careful route through the entire game, as every collected and missed item will influence the \u003ccode\u003eitem_skill\u003c/code\u003e in some way. This almost feels it's like the ultimate challenge that this game has to offer. Looking forward to the first Vanilla Harmony% run!\n\u003c/p\u003e\u003cp\u003e\n\tAnd with that, TH02's \u003ccode\u003eMAINE.EXE\u003c/code\u003e is both fully position-independent and ready for translation. There's a tiny bit of undecompiled bit of code left in the binary, but I'll leave that for rounding up a future TH02 decompilation push.\n\u003c/p\u003e\u003chr id=\"bonus-2024-04-11\"\u003e\u003cp\u003e\n\tWith one of the game's skill-based formulas decompiled, it's fitting to round out the second push with the other two. The in-game bonus tables at the end of a stage also have labels that we'd eventually like to translate, after all.\u003cbr\u003e\n\tThe bonus formula for the \u003cspan class=\"hovertext\" title=\"Yup, this bonus not granted for clearing the final stage.\"\u003e4\u003c/span\u003e regular stages is also the first place where we encounter TH02's \u003ci\u003erank\u003c/i\u003e value, as well as the only instance in PC-98 Touhou where the game actually displays a rank-derived value to the player. \u003ca class=\"customer\" href=\"https://www.twitch.tv/KirbyComment\"\u003eKirbyComment\u003c/a\u003e and Colin Douglas Howell \u003ca href=\"https://en.touhouwiki.net/index.php?title=Story_of_Eastern_Wonderland/Gameplay\u0026oldid=454863#Rank\"\u003eaccurately documented the rank mechanics over at Touhou Wiki two years ago\u003c/a\u003e, which helped quite a bit as rank would have been slightly out of scope for these two pushes. \u003ca href=\"/blog/2021-10-09\"\u003e📝 Similar to TH01\u003c/a\u003e, TH02's rank value only affects bullet speed, but the exact details of \u003ci\u003ehow\u003c/i\u003e rank is factored in will have to wait until RE progress arrives at this game's bullet system.\u003cbr\u003e\n\tThese bonuses are calculated by taking a sum of various gameplay metrics and multiplying it with the amount of point items collected during the stage. In the 4 regular stages, the sum consists of:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers bonus-2024-04-11\"\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003e\u003cspan class=\"hovertext\" title=\"What's that space doing here? None of the other labels even attempt to be centered.\"\u003e\u0026nbsp;\u003c/span\u003e難易度\u003c/th\u003e\n\t\u003ctd\u003eDifficulty level*\u003ccode\u003e × 2,000\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eステージ\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003e(\u003c/code\u003eRank\u003ccode\u003e + 16) ×   200\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eボム\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emax((2,500 - (\u003c/code\u003eBombs used*\u003ccode\u003e ×   500)), 0)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eミス\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emax((3,000 - (\u003c/code\u003eLives lost*\u003ccode\u003e × 1,000)), 0)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003e靈撃初期数\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003e(4 - \u003c/code\u003eStarting bombs\u003ccode\u003e) ×   800\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003e靈夢初期数\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003e(5 - \u003c/code\u003eStarting lives\u003ccode\u003e) × 1,000\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\t* Within this stage, across all continues.\u003cbr\u003e\n\tYup, \u003ccode\u003e封魔録.TXT\u003c/code\u003e does indeed document this correctly.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAs rank can range from -6 to +4 on Easy and +16 on the other difficulties, this sum can range between:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\"\u003e\n\t\u003ctr\u003e\u003cth\u003e\u003c/th\u003e\u003cth\u003eEasy\u003c/th\u003e\u003cth\u003eNormal\u003c/th\u003e\u003cth\u003eHard\u003c/th\u003e\u003cth\u003eLunatic\u003c/th\u003e\u003c/tr\u003e\n\t\u003ctr\u003e\n\t\t\u003cth\u003eMinimum\u003c/th\u003e\n\t\t\u003ctd\u003e2,800\u003c/td\u003e\u003ctd\u003e4,800\u003c/td\u003e\u003ctd\u003e6,800\u003c/td\u003e\u003ctd\u003e8,800\u003c/td\u003e\n\t\u003c/tr\u003e\n\t\u003ctr\u003e\n\t\t\u003cth\u003eMaximum\u003c/th\u003e\n\t\t\u003ctd\u003e16,700\u003c/td\u003e\u003ctd\u003e21,100\u003c/td\u003e\u003ctd\u003e23,100\u003c/td\u003e\u003ctd\u003e25,100\u003c/td\u003e\n\t\u003c/tr\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe sum for the Extra Stage is not documented in \u003ccode\u003e封魔録.TXT\u003c/code\u003e:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers bonus-2024-04-11\"\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eクリア\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003e10,000\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eミス回数\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emax((20,000 - (\u003c/code\u003eLives lost\u003ccode\u003e × 4,000)), 0)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eボム回数\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emax((20,000 - (\u003c/code\u003eBombs used\u003ccode\u003e × 4,000)), 0)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth lang=\"ja\"\u003eクリアタイム\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003e⌊max((20,000 - \u003c/code\u003eBoss fight frames*\u003ccode\u003e), 0) ÷ 10⌋ × 10\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\t* Amount of frames spent fighting Evil Eye Σ, counted from the end of the pre-boss dialog until the start of the defeat animation.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\u003cp\u003e\n\tAnd that's two pushes packed full of the most bloated and copy-pasted code that's unique to TH02! So bloated, in fact, that TH02 RE as a whole jumped by almost 7%, which in turn finally pushed overall RE% over the 60% mark. 🎉 It's been a while since we hit a similar milestone; 50% overall RE happened \u003ca href=\"/progress/39da5da2eaddd56d7dad3f70f8a86f05c5dad863\"\u003ealmost 2 years ago\u003c/a\u003e during \u003ca href=\"/blog/2022-07-10\"\u003e📝 P0204\u003c/a\u003e, a month before I completed the TH01 decompilation.\u003cbr\u003e\n\tNext up: Continuing to wait for Microsoft to fix the static analyzer bug until May at the latest, and working towards the newly popular dreams of TH03 netplay by looking at some of its foundational \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e code.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-04-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-03-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-04-11T22:45:29Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-03-09",
      "url": "https://rec98.nmlgc.net/blog/2024-03-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-04-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-02-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-03-09\"\u003e\u003ctime datetime=\"2024-03-09T15:23:29Z\"\u003e2024-03-09 15:23\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0266\"\u003eP0266\u003c/a\u003e\n\t\t\tmly (CLI preparation + MIDI event dumping + Basic loop detection algorithm)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/mly/compare/eb2b0c8...b356884\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0267\"\u003eP0267\u003c/a\u003e\n\t\t\tmly (Refined loop detection algorithm + SMF Type 0 conversion + Cut/unfold operations)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/mly/compare/b356884...1c70db0\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0268\"\u003eP0268\u003c/a\u003e\n\t\t\tmly (Recording-space loops + Rendering helpers) + Seihou / Shuusou Gyoku (BGM packs, part 1/?: Sound Canvas VA versions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/mly/compare/1c70db0...db0c195\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0269\"\u003eP0269\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (BGM packs, part 2/6: Loop-cutting Romantique Tp recordings + Arranged MIDI pipeline + Packaging)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/BGMPacks/compare/2f9bce5...45087c2\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0270\"\u003eP0270\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (BGM packs, part 3/6: Dependency setup + UTF-8 paths)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0256...a9ca081\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0271\"\u003eP0271\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (MIDI refactoring and bugfixes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/a9ca081...8db918f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0272\"\u003eP0272\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (MIDI looping + BGM packs, part 4/6: MIDI/waveform BGM subsystem)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/8db918f...3de48ab\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0273\"\u003eP0273\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (BGM packs, part 5/6: Modding logic + FLAC/Vorbis waveform streaming)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/3de48ab...9467705\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0274\"\u003eP0274\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (BGM packs, part 6/6: Configuration UI + Music Room adjustments)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/9467705...241a6c9\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0275\"\u003eP0275\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (BGM/sound effect volume controls + Missing .DAT file screen)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/241a6c9...P0275\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0276\"\u003eP0276\u003c/a\u003e\n\t\t\tWebsite (CSS compilation via esbuild + Audio player, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/dbc369f...883ac40\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0277\"\u003eP0277\u003c/a\u003e\n\t\t\tWebsite (Audio player, part 2/2 + Front page progress bar redesign + Goal grouping)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/883ac40...6ac72f3\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midi-projects\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"MIDI-related side projects involving visualization, manipulation, and other useful algorithms.\"\u003emidi-projects\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bgm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The music behind Touhou. Arguably the core of what motivated ZUN to create this series to begin with.\"\u003ebgm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\u003cstyle\u003e\n\t.loopquirks-2024-03-09 dl {\n\t\tdisplay: grid;\n\t\tborder: none;\n\t\twidth: max-content;\n\t\tplace-items: baseline right;\n\t\tpadding-bottom: 0;\n\t\tmax-width: 100%;\n\t}\n\t.loopquirks-2024-03-09 dl dt {\n\t\tgrid-column: 1;\n\t\tmargin-right: 1ch;\n\t}\n\t.loopquirks-2024-03-09 dl dd {\n\t\tgrid-column: 2;\n\t}\n\t.viz-2024-03-09 rec98-video:not(:fullscreen) video {\n\t\twidth: 640px;\n\t\theight: 360px;\n\t\taspect-ratio: 640 / 360;\n\t\tobject-fit: fill;\n\t}\n\t#sysex-bugs-2024-03-09 rec98-child-switcher div {\n\t\tcolor: white;\n\t\twidth: 100%;\n\t\toverflow-x: scroll;\n\t}\n\t#sysex-bugs-2024-03-09 h4 {\n\t\tmargin-block: 0;\n\t}\n\t#sysex-bugs-2024-03-09 pre {\n\t\tmargin: 0;\n\t\tpadding-top: 0.5em;\n\t\tpadding-bottom: 0.5em;\n\t}\n\u003c/style\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2022-01-31\"\u003e📝 Over two years since the previous largest delivery\u003c/a\u003e, we've now got a new record in every regard: 12 pushes across 5 repos, 215 commits, and a blog post with over 14,000 words and 48 pieces of media. 😱 Who would have thought that the superficially simple task of putting SC-88Pro recordings into Shuusou Gyoku would actually mainly focus on deep research into the underlying MIDI files? I don't typically cover much music-related content because it's a non-issue as far as PC-98 Touhou code is concerned, so it's quite fitting how extensive this one turned out. So here we go, the result of virtually unlimited funding and patience:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#controversy-2024-03-09\"\u003eThe SC-88Pro recording controversy\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sysex-2024-03-09\"\u003eUndefined SysEx behavior\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#choice-2024-03-09\"\u003eResolving the controversy, and making a choice (contains personal opinion)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#mly-2024-03-09\"\u003eA Unix-style command-line MIDI filter (in Rust BTW)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#midiviz-2024-03-09\"\u003eVisualizing MIDI files (for science, and not for playing them on a keyboard)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#loopquirks-2024-03-09\"\u003eShuusou Gyoku's individual loop quirks 🎺\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pbgmidi-2024-03-09\"\u003eRewriting pbg's MIDI code\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#bgmpacks-2024-03-09\"\u003ePutting together the BGM packs\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#libs-2024-03-09\"\u003eOutgrowing miniaudio (and raging about single-file C libraries for a while)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#impl-2024-03-09\"\u003eRemaining implementation details\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pricing-2024-03-09\"\u003ePricing changes (and no, not everything's getting more expensive)\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"controversy-2024-03-09\"\u003e\u003cp\u003e\n\tSo where's the controversy? Romantique Tp obviously made \u003ca href=\"https://www.shrinemaiden.org/forum/index.php?topic=18989.0\"\u003ethe best and most careful real-hardware SC-88Pro recordings of all of ZUN's old MIDIs\u003c/a\u003e, including the original (\u003cdfn\u003eOST\u003c/dfn\u003e) and \u003ca href=\"https://www16.big.or.jp/~zun/html/music_old.html\"\u003earranged\u003c/a\u003e (\u003cdfn\u003eAST\u003c/dfn\u003e) soundtrack of Shuusou Gyoku, right? Surely all I have to do now is to cut them into seamless loops to save a bit of disk space, and then put them into the game? Let's start at the end of the track list with the name registration theme, since it's light on instruments and has an obvious loop point that will be easy to spot in the waveform. But, um… wait a moment, that very first drum note comes a bit late, doesn't it?\n\u003c/p\u003e\u003cfigure class=\"waveform-2024-03-09\" \u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThis can also be heard in \u003ca href=\"https://www.youtube.com/watch?v=eXU3XDJy6Cg\"\u003eRomantique Tp's YouTube upload.\u003c/a\u003e\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tAt a notated tempo of 96 BPM, these first four beats should take exactly 2.5 seconds, which they do in this seamlessly looping softsynth rendering.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o20-RomantiqueTp.flac?253da93f\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o20-RomantiqueTp.png?59483da9\" preload=\"none\" controls data-title=\"Romantique Tp recording\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o20-SCVA.flac?5b8778c1\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o20-SCVA.png?18fbf944\" preload=\"none\" controls data-title=\"Sound Canvas VA\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's… not quite the accuracy and perfection I was expecting. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e But I think I know what we're seeing and hearing there. Let's look at the first few MIDI events across all channels:\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cpre\u003eDelta\tPulse\t Beat\tChannel\tEvent\n +540\t   960\t  2:000\t      1\tController { CC   0, value   0 }\n   +0\t   960\t  2:000\t      1\tController { CC  32, value   0 }\n   +0\t   960\t  2:000\t      1\tProgramChange {  37 }\n[…]\n   +0\t   960\t  2:000\t      2\tController { CC   0, value   0 }\n   +0\t   960\t  2:000\t      2\tController { CC  32, value   0 }\n   +0\t   960\t  2:000\t      2\tProgramChange {  19 }\n[…]\n   +0\t   960\t  2:000\t      3\tController { CC   0, value   0 }\n   +0\t   960\t  2:000\t      3\tController { CC  32, value   0 }\n   +0\t   960\t  2:000\t      3\tProgramChange {   6 }\n[…]\n   +0\t   960\t  2:000\t      4\tController { CC   0, value   0 }\n   +0\t   960\t  2:000\t      4\tController { CC  32, value   0 }\n   +0\t   960\t  2:000\t      4\tProgramChange {   2 }\n[…]\u003c/pre\u003e\n\t\u003cpre\u003eDelta\tPulse\t Beat\tChannel\tEvent\n   +0\t  960\t2:000\t     10\tController { CC   0, value   0 }\n   +0\t  960\t2:000\t     10\tController { CC  32, value   0 }\n   +0\t  960\t2:000\t     10\tProgramChange {  25 }\n   +0\t  960\t2:000\t     10\tController { CC   7, value 127 }\n   +0\t  960\t2:000\t     10\tController { CC  11, value 127 }\n   +0\t  960\t2:000\t     10\tController { CC  10, value  64 }\n   +0\t  960\t2:000\t     10\tController { CC  91, value  80 }\n   +0\t  960\t2:000\t     10\tController { CC  93, value  40 }\n   +0\t  960\t2:000\t     10\tNoteOn { Key  42, Vel.  94 }\n   +0\t  960\t2:000\t     10\tNoteOn { Key  36, Vel. 110 }\n   +1\t  961\t2:001\t     10\tNoteOn { Key  42, Vel.   0 }\n   +0\t  961\t2:001\t     10\tNoteOn { Key  36, Vel.   0 }\n +119\t 1080\t2:120\t     10\tNoteOn { Key  42, Vel.  34 }\n   +1\t 1081\t2:121\t     10\tNoteOn { Key  42, Vel.   0 }\n +119\t 1200\t2:240\t     10\tNoteOn { Key  42, Vel.  64 }\n   +0\t 1200\t2:240\t     10\tNoteOn { Key  36, Vel.  64 }\u003c/pre\u003e\n\t\u003cfigcaption\u003e\n\tAlso, the fact that GS doesn't put its drums on a non-general voice bank and instead relies on external channel configuration to differentiate drums from pitched instruments is making this Yamaha kid uncontrollably furious. 🤬\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tYup. That's the sound of a vintage hardware synth being slow and taking a two-digit number of milliseconds to process a barrage of simultaneous Program Change messages, playing a MIDI file that doesn't take this reality into account and expects program changes to happen instantly.\u003cbr\u003e\n\tI can only speak from my own experience of writing MIDIs for hardware synths here, but having the first note displaced by 50\u0026nbsp;ms is very much \u003ci\u003enot\u003c/i\u003e the way a composer would have \u003ci\u003eintended\u003c/i\u003e the music to be heard if the note is clearly notated to occur \u003ci\u003eon\u003c/i\u003e the beat. If you had told me about such an issue when playing one of my MIDIs on a certain synth, I would have thanked you for the bug report! And I would have promptly released a fixed version of the MIDI with the Program Change events moved back by a beat or two. In the case of Shuusou Gyoku's MIDIs, this wouldn't even have added any additional delay in-game, as all of these files already start with at least one beat of leading silence to make room for setting Roland-specific synth parameters.\n\u003c/p\u003e\u003cp\u003e\n\tOK, but that's just a single isolated bass drum hit. If we wanted to, we could even fix this issue ourselves by splicing the same note from around the loop end point. Maybe this is just an isolated case and the rest of Romantique Tp's recordings are fine? Well…\n\u003c/p\u003e\u003cfigure class=\"waveform-2024-03-09\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tAgain, \u003ca href=\"https://www.youtube.com/watch?v=xFovQWYiJEw\"\u003echeck Romantique Tp's YouTube upload for proof.\u003c/a\u003e\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tBy the way, this seamless audio player is what consumed most of the two website pushes this time. The rest went to the slightly redesigned main page, whose progress bars now use the cap bar style and the GitHub badge colors.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o18-RomantiqueTp.flac?297a3f1e\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o18-RomantiqueTp.png?fd64d6b0\" preload=\"none\" controls data-title=\"Romantique Tp recording\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o18-SCVA.flac?dce7c102\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o18-SCVA.png?6f603368\" preload=\"none\" controls data-title=\"Sound Canvas VA\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis one is even worse. Here, the delay is so long relative to the tempo of the piece that the intended five drum hits pretty much turn into four.\n\u003c/p\u003e\u003cp\u003e\n\tThis type of issue doesn't even have to be isolated to the very beginning of a piece. A few of the tracks in both the OST and AST start with an \u003ca href=\"https://en.wikipedia.org/wiki/Anacrusis#Music\"\u003eanacrusis\u003c/a\u003e on just one or two channels and leave the Program Change event barrage at the beginning of the first full measure. In \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Stage 6 Boss/VIVIT-captured-'s second theme\"\u003e幻想科学 ～ Doll's Phantom\u003c/span\u003e for example, this creates a flam-like glitch where the bass on channel 2 is pretty much on time, but the crash hit on channel 10 only follows 50\u0026nbsp;ms later, after the SC-88Pro took its sweet time to process all the Program Change events on the channels between:\n\u003c/p\u003e\u003cfigure class=\"waveform-2024-03-09\"\u003e\n\t\u003cfigcaption\u003eThis is from the arranged soundtrack for a change. In that one, ZUN at least fixed the issue in the final three MIDIs (\u003cspan lang=\"ja\" class=\"hovertext\" title=\"Extra Stage theme\"\u003eシルクロードアリス\u003c/span\u003e, \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Marisa's theme\"\u003e魔女達の舞踏会\u003c/span\u003e, and \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Reimu's theme\"\u003e二色蓮花蝶　～ Ancients\u003c/span\u003e) that closed out this rearranging project in May 2001, which spread out their per-channel setup events over at least a single measure before playing any note.\u003c/figcaption\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-a14-RomantiqueTp.flac?c9e8a196\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-a14-RomantiqueTp.png?c2511dda\" preload=\"none\" controls data-title=\"Romantique Tp recording\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-a14-SCVA.flac?75633229\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-a14-SCVA.png?cd1239f9\" preload=\"none\" controls data-title=\"Sound Canvas VA\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp id=\"halfspeed-2024-03-09\"\u003e\n\tLet's listen to that at half speed:\n\u003c/p\u003e\u003cfigure class=\"waveform-2024-03-09\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\t\u003ca href=\"https://www.youtube.com/watch?v=zpkAyjPZiaM\"\u003eRomantique Tp's YouTube upload.\u003c/a\u003e\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tStill on point.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-a14-half-RomantiqueTp.flac?e2ea5330\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-a14-half-RomantiqueTp.png?48d5721a\" preload=\"none\" controls data-title=\"Romantique Tp recording\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-a14-half-SCVA.flac?bd73959d\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-a14-half-SCVA.png?b56c6682\" preload=\"none\" controls data-title=\"Sound Canvas VA\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSure, all of this is barely noticeable in casual listening, but \u003ci\u003every\u003c/i\u003e noticeable if you're the one who now has to cut these recordings into seamless loops. And these are just the most obvious timing issues that can be easily pinpointed and documented – the \u003ci\u003eactual\u003c/i\u003e worst aspects are all the minor tempo and timing fluctuations \u003ci\u003ethroughout\u003c/i\u003e most of the pieces. With recordings that deviate ever so slightly from the tempo defined in the MIDI files, you can no longer rely on mathematically exact sample positions when cutting loops. Even if those positions do work out from time to time, there'd pretty much always be a discontinuity in the waveform at both ends of the loop, manifesting as a clearly audible click. In the end, the only way of finding good loop points in existing recordings involves straining your ears and listening very, \u003ci\u003every\u003c/i\u003e closely to avoid any audible glitches. 😩\n\u003c/p\u003e\u003cp\u003e\n\tBut if you've taken a look at the second tabs in the clips above, you will have noticed that we don't necessarily have to be stuck with recordings from real hardware. In late 2015, Roland released \u003ca href=\"https://www.roland.com/products/rc_sound_canvas_va/\"\u003e\u003ccite\u003eSound Canvas VA\u003c/cite\u003e\u003c/a\u003e, a VST plugin that emulates the classic core of Roland's old Sound Canvas lineup, including the SC-88Pro. As long as we run such a software synthesizer through \u003ca href=\"https://github.com/stuerp/foo_midi\"\u003ea quality VST host\u003c/a\u003e, a purely software-based solution should be way superior for recording looped BGM:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eBy moving from real-time recording to an offline rendering paradigm, we get perfectly accurate note timing, as it no longer matters how long the synth takes to produce each output sample.\u003c/li\u003e\n\t\u003cli\u003eWe stay entirely in the digital realm instead of going from digital (SC-88Pro) to analog (RCA cable) to digital (line-in recording) again, removing any chance for noise or distortion to ruin audio quality.\u003c/li\u003e\n\t\u003cli\u003eWe get to directly render at 44,100\u0026nbsp;Hz instead of being limited to the 32,000\u0026nbsp;Hz signal coming out of the SC-88Pro's DAC. This can be easily noticed in the \u003ca href=\"#halfspeed-2024-03-09\"\u003ehalf-speed video above\u003c/a\u003e, whose SCVA version retains significantly more sibilant high-frequency content compared to the more muffled sound of Romantique Tp's recording.\u003c/li\u003e\n\t\u003cli\u003eGood recordings from real hardware involve careful observation of the output volume. You ideally want to \u003ca href=\"https://www.shrinemaiden.org/forum/index.php?topic=18989.msg1285441#msg1285441\"\u003eset the synth's master volume to a level that's simultaneously quiet enough to avoid clipping \u003ci\u003eand\u003c/i\u003e loud enough to ensure the highest possible signal-to-noise ratio\u003c/a\u003e, which requires lots of trial-and-error attempts for each individual track. With a softsynth, this becomes a non-issue: \u003ca href=\"/blog/2023-09-30#resampling-2023-09-30\"\u003e📝 As we've seen during the last look at Shuusou Gyoku's sound\u003c/a\u003e, rendering to 32-bit floating-point .WAV files would preserve all waveform information above 0\u0026nbsp;dBFS. So we could simply render everything at Sound Canvas VA's default maximum volume and later algorithmically scale the volume of the rendered files to fit within 0\u0026nbsp;dBFS, without having to touch SCVA's sluggish interface.\u003cul\u003e\n\t\t\u003cli\u003eDoing that also makes it feasible to preserve loudness differences \u003ci\u003ebetween\u003c/i\u003e the pieces of a soundtrack instead of eradicating them by normalizing the volume of each \u003ci\u003eindividual\u003c/i\u003e track to the digital maximum.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eFinally, it's much more time-efficient. We simply hit foobar2000's \u003ci\u003eConvert\u003c/i\u003e button and get all MIDIs rendered within a few seconds each, instead of having to wait the entire length of a piece.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAny drawbacks? For our use case, all of them are found in the abysmal software quality of everything \u003ci\u003earound\u003c/i\u003e the synth engine. As it's typical for the VST industry, Sound Canvas VA is excessively DRM'd – it takes multiple seconds to start up, and even then only allows a single process to run at any given time, immediately quitting every process beyond the first one with a misleading \u003ci\u003eParameter File1 Read Error\u003c/i\u003e message box. I totally believe anyone who claims that this makes SCVA more annoying than real hardware when composing new music. Retro gamers also dislike how Roland themselves no longer sells the 32-bit builds they used to offer for the first few versions. These old versions are now exclusively available through resellers, or on the seven seas.\u003cbr\u003e\n\tBut as far as the SC-88Pro emulation is concerned, there don't seem to be any technical reasons against it. There is \u003ca href=\"https://www.vogons.org/viewtopic.php?f=62\u0026t=46111\"\u003ea long thread over at VOGONS discussing all sorts of issues\u003c/a\u003e, but you have to dig quite deep to find any clear descriptions of bugs in SCVA's synth engine. Everything I found either \u003ca href=\"https://www.vogons.org/viewtopic.php?p=1197315#p1197315\"\u003eonly applies to the SC-55 emulation and not the SC-88Pro\u003c/a\u003e, \u003ca href=\"https://www.vogons.org/viewtopic.php?p=498459#p498459\"\u003ewas fixed by Roland in the meantime\u003c/a\u003e, or \u003ca href=\"https://www.vogons.org/viewtopic.php?p=1183092#p1183092\"\u003eturned out to be a fixable bug in a MIDI file\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\t\u003cs\u003eNevertheless, Romantique Tp has \u003ca href=\"https://www.shrinemaiden.org/forum/index.php?topic=18989.msg1276848#msg1276848\"\u003ea very negative opinion about SCVA, getting quite angry and defensive in this instance where someone favorably compared SCVA to their recordings\u003c/a\u003e.\u003c/s\u003e\n\t\u003cstrong\u003eEdit (2024-03-10):\u003c/strong\u003e \u003ca href=\"https://twitter.com/Romantique_Tp/status/1766898006329053256\"\u003eThese days, Romantique Tp has a much more favorable opinion on SCVA as well.\u003c/a\u003e\u003cbr\u003e\n\t8 years after their release, however, the community unanimously accepts the Romantique Tp recordings as the intended way to listen to ZUN's old MIDIs, so choosing Sound Canvas VA for our Shuusou Gyoku builds might be a bad idea purely for PR reasons. At best, people would slightly wonder why I intentionally went with the opposite of the accepted reference recordings, but at worst, this entire project could face a violent backlash…\n\u003c/p\u003e\u003chr id=\"sysex-2024-03-09\"\u003e\u003cp\u003e\n\tBut wait, we've already heard one obvious difference between the real SC-88Pro and Sound Canvas VA. Let's listen to the very first clip again:\n\u003c/p\u003e\u003cfigure class=\"waveform-2024-03-09\"\u003e\n\t\u003crec98-audio class=\"rec98-player\"\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o20-RomantiqueTp.flac?253da93f\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o20-RomantiqueTp.png?59483da9\" preload=\"none\" controls data-title=\"Romantique Tp recording\" loop data-active\u003e\u003c/audio\u003e\u003caudio src=\"/blog/static/audio/2024-03-09-SH01-o20-SCVA.flac?5b8778c1\" data-waveform=\"/blog/static/audio/2024-03-09-SH01-o20-SCVA.png?18fbf944\" preload=\"none\" controls data-title=\"Sound Canvas VA\" loop\u003e\u003c/audio\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-audio\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHa! You can clearly hear a panning echo in the real-hardware recording that is missing from the Sound Canvas VA rendering. That's an obvious case of a core system effect not being reproduced correctly. If even \u003ci\u003ethat's\u003c/i\u003e undeniably broken, who knows which other subtle bugs SCVA suffers from, right? Case closed, Romantique Tp was right all along, SCVA is trash, real hardware reigns supreme \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tActually, let's look closer into this one. Panning delay effects like this are typically reverb-related, but General MIDI only specifies a single controller to specify the per-channel reverb \u003ci\u003elevel\u003c/i\u003e from 0 to 127. Any specific characteristics of the reverb therefore have to be configured using vendor-specific system-exclusive messages, or \u003ci\u003eSysEx\u003c/i\u003e for short.\u003cbr\u003e\n\tSo it's down to one of the four SysEx messages at the beginning of the MIDI file:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003eDelta\tPulse\t Beat\tEvent\n   +0\t    0\t0:000\tSysEx(41 10 42 12 40 00 7F 00 41 F7)\n +240\t  240\t0:240\tSysEx(41 10 42 12 40 01 30 14 7B F7)\n +120\t  360\t0:360\tSysEx(41 10 42 12 40 01 33 0F 7D F7)\n  +60\t  420\t0:420\tSysEx(41 10 42 12 40 01 34 30 5B F7)\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSince these byte strings represent Roland-specific instructions, we can't learn anything from a raw MIDI event dump alone here. No problem though, let's just load these files into some old MIDI sequencer that targeted Roland synths, open its MIDI event list, and then they will be automatically decoded into a human-readable representation…\u003cbr\u003e\n\t…or at least that's what I expected. In Yamaha land, XGworks has done that for \u003ca href=\"https://en.wikipedia.org/wiki/Yamaha_XG\"\u003eYamaha's own XG\u003c/a\u003e SysEx messages ever since 1997:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 698px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-03-09-XGworks-SysEx.png?08479b28\"\n\talt=\"Screenshot of the MIDI Event Viewer in Yamaha's XGworks, showing off its automatic XG SysEx decoding feature.\"\u003e\n\t\u003cfigcaption\u003eNo configuration required. You can even edit the textual \u003ccode\u003eValue1\u003c/code\u003e representation and XGworks parses it back into the closest supported value!\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut for Roland synths, there's… nothing similar? Seriously? 😶 Roland fanboys, how do you even \u003ci\u003elive\u003c/i\u003e?! I mean, they are quick to recommend the typical bloated and sluggish big-name DAWs that take up multiple gigabytes of disk space, but none of the ones I tried seemed to have this feature. They can't have \u003ci\u003epossibly\u003c/i\u003e been flinging around raw byte strings for the past 33 years?!\u003cbr\u003e\n\tBut once you look more into today's MIDI community, it becomes clear that this is exactly what they've been doing. Why else would so many people use the word \u003ca href=\"https://mididesigner.com/qa/7181/most-basic-sysex-questions?show=7260\"\u003e\u003cq\u003ecomplicated\u003c/q\u003e\u003c/a\u003e to describe Roland SysEx, or call it \u003cq\u003e\u003ca href=\"https://scpurist.wordpress.com/2018/06/10/activating-the-efx-control-switches/\"\u003ean old school/cryptic communication protocol in hexadecimal format\u003c/a\u003e\u003c/q\u003e? The latter is particularly hilarious because if you removed the word \u003ci\u003ecryptic\u003c/i\u003e, this might as well describe all of MIDI, not just SysEx. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Everything about this is a tooling issue, and Yamaha showed how easily it could have been solved. Instead, we get \u003ca href=\"https://scpurist.wordpress.com/2017/05/13/software-bugs-updates-on-sound-canvas-va/\"\u003eSound Canvas experts\u003c/a\u003e, who \u003ci\u003eshould\u003c/i\u003e know more about the ecosystem than I do, making the incredible mental leap from \u003ci\u003e\"my DAW doesn't decode or easily generate SysEx\"\u003c/i\u003e to \u003ci\u003e\"SysEx is antiquated\"\u003c/i\u003e to \u003ci\u003e\"please just lift up these settings to the VST level and into my proprietary DAW's proprietary project format, that would be so much better\"\u003c/i\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tThankfully that's not entirely true. After some more digging and configuration, I found a somewhat workable solution involving a comparatively modern sequencer called \u003ccite\u003eDomino\u003c/cite\u003e:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eDownload either \u003ca href=\"https://takabosoft.com/domino\"\u003eDomino's original Japanese version\u003c/a\u003e or \u003ca href=\"https://github.com/Hans5958/Domino-English-Translation/releases/tag/en.5-nightly.20230403\"\u003ethe partial English translation\u003c/a\u003e. The .zip file on the release page contains a full standalone build.\u003c/li\u003e\n\t\u003cli\u003eOpen the \u003ci\u003eFile → Preferences\u003c/i\u003e menu and associate your MIDI output device with a module map. This makes sense for SysEx \u003ci\u003een\u003c/i\u003ecoding/generation since it can limit the options in the UI to what's actually available on your target hardware, but is also required for selecting the respective SysEx map into Domino's SysEx \u003ci\u003ede\u003c/i\u003ecoder. There is no \u003ci\u003etechnical\u003c/i\u003e reason for this because SC-88Pro SysEx messages can be uniquely identified by the three vendor, device, and model ID bytes that every message starts with, but would be too easy and user-friendly. The perception of SysEx being \u003ca href=\"https://www.youtube.com/watch?v=JKJMeHwydUQ\"\u003ea black art\u003c/a\u003e must be upheld at all costs.\n\t\t\u003cfigure style=\"width: 296px;\"\u003e\n\t\t\t\u003cimg\n\t\t\t\tsrc=\"/blog/static/2024-03-09-Domino-1-Model-map.png?a5cf5a08\"\n\t\t\t\talt=\"Screenshot of Domino's MIDI-OUT window, complete with garbled text\"\n\t\t\t\u003e\n\t\t\t\u003cfigcaption\u003eI've kept the garbled text of the partial translation to emphasize the sheer amount of jank involved in this entire process.\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eLoad a MIDI file and let Domino \"analyze\" it:\n\t\t\u003cfigure style=\"width: 203.5px\"\u003e\n\t\t\t\u003cimg\n\t\t\t\tsrc=\"/blog/static/2024-03-09-Domino-2-Analyze.png?23344b0b\"\n\t\t\t\talt=\"Screenshot of Domino's analysis message box\"\n\t\t\t\u003e\n\t\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eStrangely enough, this will take quite a while – on my system, this analysis step runs at a speed of roughly 4.25 KB/s of MIDI data. Yes, \u003ci\u003ekilobytes\u003c/i\u003e.\u003c/li\u003e\n\t\u003cli\u003eUnfortunately, \"control change macro restoration\" also seems to mean that you don't get to see \u003ci\u003eany\u003c/i\u003e raw bytes when selecting the respective MIDI track in the UI, but at least we get what we were looking for:\n\t\t\u003cfigure class=\"side_by_side\"\u003e\n\t\t\t\u003cfigure class=\"pixelated\" style=\"width: 322px\"\u003e\n\t\t\t\t\u003cimg\n\t\t\t\t\tsrc=\"/blog/static/2024-03-09-Domino-3-Decoded-SysEx.png?102d2ee6\"\n\t\t\t\t\talt=\"Screenshot of the four SysEx messages of タイトルドメイド, Shuusou Gyoku's name registration theme, as decoded by Domino\"\n\t\t\t\t\u003e\n\t\t\t\t\u003cfigcaption\u003e…for the most part?\u003c/figcaption\u003e\n\t\t\t\u003c/figure\u003e\u003cfigure\u003e\n\t\t\t\t\u003cpre\u003ePulse\tEvent\n    0\tSysEx(41 10 42 12 40 00 7F 00 41 F7)\n  240\tSysEx(41 10 42 12 40 01 30 14 7B F7)\n  360\tSysEx(41 10 42 12 40 01 33 0F 7D F7)\n  420\tSysEx(41 10 42 12 40 01 34 30 5B F7)\u003c/pre\u003e\n\t\t\t\u003c/figure\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tAlright, that's something we can work with. The \u003ci\u003eGS Reset\u003c/i\u003e message is something that every Roland GS MIDI should start with, but it's immediately followed by a message that Domino failed to decode? The two subsequent reverb parameters make sense, but panning delays typically have more parameters than just a reverb level and time.\u003cbr\u003e\n\tThat unknown SysEx message shares much of the same bytes with the decoded ones though. So let's do what we maybe should have done all along, return to caveman, and check the \u003ca href=\"https://cdn.roland.com/assets/media/pdf/SC-88PRO_OM.pdf\"\u003eSC-88Pro manual\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure\u003e\u003cembed src=\"/blog/static/2024-03-09-SC88Pro-Reverb-SysEx.svg?73f48b6e\"\u003e\u003cfigcaption\u003e\n\tThe relevant section from page 194. We can see how the address and value correspond to bytes 5-7 and 8 in the SysEx messages. Byte 9 is a checksum and byte 10 signals the end of the message.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that's where we find what this particular issue boils down to. The missing SysEx message is clearly intended to be a Reverb Macro command, whose value can range from 0 to 7 inclusive on the SC-88Pro, but ZUN tries to specify Reverb Macro #\u003ccode\u003e14h\u003c/code\u003e, or 20 in decimal. The SC-88Pro manual does not specify what happens if a SysEx message wants to write an invalid value to a valid address, which means that we've firmly entered the territory of undefined behavior.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2024-03-10):\u003c/strong\u003e \u003ca href=\"https://twitter.com/Romantique_Tp/status/1766895996645056902\"\u003eRomantique Tp confirmed that the real SC-88Pro clamps these Reverb Macro IDs to the supported range of 0-7.\u003c/a\u003e Therefore, the appropriate course of action for guaranteeing the same sound on other Roland synths would be to fix the MIDI file and specify Reverb Macro #7 instead. But since this behavior remains technically undefined, we can still argue about ZUN's intention behind specifying the Reverb Macro like this:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eClearly, ZUN did want to specify a valid Reverb Macro, but made a typo when manually entering the SysEx byte string, as he was forced to do thanks to terrible tooling. He clearly liked the resulting sound though, so the track should still be preserved with the panning reverb intact.\u003c/li\u003e\n\t\u003cli\u003eClearly, the typical behavior for MIDI synths is to ignore invalid and unsupported SysEx messages, because validating user input is an important characteristic of quality software. This is what SCVA does, and what we hear in its rendering is the default hall reverb with ZUN's level and time adjustments. Therefore, SCVA is right, and the fact that we get a panning delay on the real SC-88Pro is a bug in real hardware.\u003c/li\u003e\n\t\u003cli\u003eClearly, ZUN did not care enough about the reverb to specify a valid Reverb Macro. Whether we get the default reverb or a panning delay is an irrelevant performance detail, and does intentionally not matter when it comes to the intended sound of this track – especially since these four SysEx messages are the full extent of Roland GS-specific sound design in this piece, and the rest of it only uses standard MIDI features.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn fact, 32 out of the 39 MIDIs across both of Shuusou Gyoku's soundtrack use this invalid Reverb Macro. The only ones that don't are\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eboth versions of Gates' theme (\u003cspan lang=\"ja\"\u003e天空アーミー\u003c/span\u003e), which use the equally invalid Reverb Macro #11,\u003c/li\u003e\n\t\u003cli\u003eboth versions of Milia's theme (\u003cspan lang=\"ja\"\u003eプリムローズシヴァ\u003c/span\u003e), which use Reverb Macro #0 (Room 1),\u003c/li\u003e\n\t\u003cli\u003eand, again, the three arranged MIDIs that ZUN released last (\u003cspan lang=\"ja\" class=\"hovertext\" title=\"Extra Stage theme\"\u003eシルクロードアリス\u003c/span\u003e, \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Marisa's theme\"\u003e魔女達の舞踏会\u003c/span\u003e, and \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Reimu's theme\"\u003e二色蓮花蝶　～ Ancients\u003c/span\u003e), which feature a more detailed effect setup with custom chorus and EQ settings. In the case of Reimu's theme, these settings are even commented within the MIDI file.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's where this quest seemed to end, until \u003ca href=\"https://twitter.com/Romantique_Tp/status/1737161116575236559\"\u003eRomantique Tp themselves came in\u003c/a\u003e and suggested that I take a closer look at the \u003ccite\u003eGS Advanced Editor\u003c/cite\u003e, or GSAE for short.\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 488px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-03-09-GSAE.png?6f02ce03\" alt=\"The splash screen of GSAE version 4.01e.\"\u003e\n\t\u003cfigcaption\u003eMake sure to connect a MIDI input device before starting GSAE, or it will silently crash immediately after this splash screen. At least it accepts \u003ci\u003eany\u003c/i\u003e controller, so this might just be a bug instead of the typical user-hostile kind of hardware dongle DRM that is pervasive in today's synth industry. 1999 would seem a bit too early for that, thankfully.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tI was aware of this tool, but hadn't initially considered it because it's always described as just a SysEx generator/encoder. In fact, the very existence of such a tool made no sense to me at first, and seemed to prove my point that the usability of GS SysEx was wholly inferior to what I was used to in Yamaha land. Like, why not build at least a tiny and stripped-down MIDI sequencer around this functionality that would allow you to insert SC-88Pro-specific messages at any point within a sequence, and not just the beginning? I can see the need for such a tool in today's world of closed-source DAWs where hardware MIDI modules are niche and retro and are only kept alive by a small community of enthusiasts. But why would its developers guarantee that MIDI composers would have to hop between programs even back in 1997? I can only imagine that they saw how every just slightly advanced MIDI sequencer or DAW back then already used its own project format instead of raw Standard MIDI Files, and assumed that composers would therefore be program-hopping anyway?\u003cbr\u003e\n\tHowever, GSAE does support the \u003ci\u003eimport\u003c/i\u003e of settings from a MIDI file and features a SysEx history window that decodes every newly processed Roland SysEx byte string, which is all I was looking for. So let's throw in that same MIDI and…\n\u003c/p\u003e\u003cfigure class=\"pixelated\" style=\"width: 507px;\"\u003e\n\t\u003cimg src=\"/blog/static/2024-03-09-SH01-o20-GSAE-SysEx.png?fa56fa75\" alt=\"Screenshot of GSAE's SysEx history window,showing the results of sending a GS Reverb Macro #20 message\"\u003e\n\t\u003cfigcaption\u003eThat's the result of sending just the single \u003ccode\u003eF0 41 10 42 12 40 01 30 14 7B F7\u003c/code\u003e message at the top.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow that's some wild numbers. An equally invalid Reverb Character, and Reverb Level and Time values that even exceed their defined range of 0-127? Could it be that GSAE emulates the real-hardware response to invalid Reverb Macros here, and gives us the exact reverb setting we can hear in Romantique Tp's recording? This could even be the reason why GSAE is still used and recommended within today's Roland MIDI sequencing scene, and hasn't been supplanted by some more modern open-source tool written by the community.\n\u003c/p\u003e\u003cp\u003e\n\tIn any case, these values have to come from somewhere, so let's reverse-engineer GSAE and figure out the logic behind them. Shoutout to \u003ca href=\"https://github.com/crypto2011/IDR\"\u003eIDR\u003c/a\u003e for being a great help with its automatic generation of IDC debug symbols for the Delphi standard library, and even including a few names of application-level widget class methods by reading Delphi-specific type information from the binary. This little sub-project made me also come around to appreciating Ghidra, whose decompiler and data type manager helped a lot and allowed me to find the relevant code section within just a few hours.\u003cbr\u003e\n\tA~nd it turns out that the values all come from out-of-bounds accesses into arrays on the stack. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e If we combine 25, 235, and 132 back into a 32-bit value, we get \u003ccode\u003e0x19EB84\u003c/code\u003e, which is the \u003cspan class=\"hovertext\" title=\"Remember, it's 1999 and ASLR isn't a thing yet.\"\u003evirtual address\u003c/span\u003e of the relevant function's stack frame base pointer.\u003cbr\u003e\n\tBut it gets even more hilarious: If you enable debug text output via \u003ci\u003eOption → Other Options → SMF → Insert text events to setup measures\u003c/i\u003e and export these imported settings back into a MIDI file, GSAE not only retains these invalid Reverb Macro IDs, but stringifies them via a simple lookup into a hardcoded string pointer array, again without any bounds checks. The effects of this are roughly what you would expect:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eReverb Macro IDs between 8 and 27 simply insert wrong strings from adjacent string pointer arrays\u003c/li\u003e\n\t\u003cli\u003eReverb Macro 28 crashes GSAE\u003c/li\u003e\n\t\u003cli\u003eReverb Macro 64 causes GSAE to vomit 65,512\u0026nbsp;bytes of garbage into the MIDI file \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn the end, we have Domino not decoding the Reverb Macro message, and GSAE, the premier SysEx tool for Roland synths, responding to it in even more undefined and clearly bugged ways than real hardware apparently does. That's two programs confirming that whatever ZUN intended was never supposed to work reliably. And while we still don't know \u003ci\u003eexactly\u003c/i\u003e what these reverb parameters are supposed to be, these observations solve the mystery as far as I'm concerned, and solidify my personal opinion on the matter.\n\u003c/p\u003e\u003chr id=\"choice-2024-03-09\"\u003e\u003cp\u003e\n\tSo what do we do now, and which version do we go with? Optimally, I'd offer both versions and turn this controversy into a personal choice so that everybody wins… and Ember2528 agreed and generously provided all the funding to make it happen. 💸\u003cbr\u003e\n\tIf you haven't picked your favorite yet, here are some final arguments:\n\u003c/p\u003e\u003cp\u003e\n\tThe Romantique Tp recordings certainly have something going for them with their provenance of coming from real hardware, and the care that Romantique Tp put into manually recording every single track, warts and all. I wholeheartedly agree that preserving the raw sound of playing the MIDI files into the hardware without thinking about bugs or quirks is an important angle to take when it comes to preservation. It's good that these recordings exist – after all, you wouldn't know which musical elements you'd possibly be missing in an emulation if you have nothing to compare it to. Even the muffled sound in the half-speed clip above can be an argument in their favor, as the SC-88Pro's DAC operates at 32\u0026nbsp;kHz and you wouldn't expect any meaningful frequency content between 16,000 and 22,050\u0026nbsp;Hz to begin with. Any frequency content in that range that does remain in Romantique Tp's recording is simply \u003ca href=\"/blog/2023-09-30#resampling-2023-09-30\"\u003e📝 rolled-off imaging noise added during the ADC's resampling process\u003c/a\u003e.\u003cbr\u003e\n\tAll this is why they are a definite improvement over \u003ca href=\"http://kaorin.pestermom.com/Music/ssg_mp3/\"\u003ekaorin's 2007 recordings of only the AST\u003c/a\u003e, which used to be the previous reference recordings within the community. Those had all of the same timing issues and more, in addition to being so excessively volume-boosted that 0.15% of the samples across the entire soundtrack ended up clipped. That's 6.25 seconds out of 68:39m being lost to pure digital noise.\n\u003c/p\u003e\u003cp\u003e\n\tMost importantly though: ZUN himself said that only the real SC-88Pro will play back these files as he intended them to sound. This quote is likely where the tagline of Romantique Tp's entire recording project came from in the first place:\n\u003c/p\u003e\u003cblockquote\u003e\u0026gt; \u003cspan lang=\"ja\"\u003e全てのデエタはSC-88ProもしくはSC-8850（ロオランド社）にて最適に聴けるように調整してあります\u003c/span\u003e\n\t\u0026gt; \u003cspan lang=\"ja\"\u003eそれ以外の音源でも、\u003cstrong\u003e作者の意図した音\u003c/strong\u003eではない場合があります。\u003c/span\u003e\n\t— ZUN on \u003ca href=\"https://www16.big.or.jp/~zun/html/music_old.html\"\u003e\u003cspan class=\"ja\"\u003e東方幻想的音楽\u003c/span\u003e, his old MIDI page\u003c/a\u003e\n\u003c/blockquote\u003e\u003cp\u003e\n\t\u003ci\u003eHowever.\u003c/i\u003e ZUN is not exactly known for accurately and carefully preserving the legacy of his series, or really doing anything beyond parading his old games as unobtainable showpieces at conventions. With all the issues we've seen, preferring real hardware is ultimately just that: \u003ci\u003ean\u003c/i\u003e angle, and \u003ci\u003ea\u003c/i\u003e preference. This is why I disagree with the \u003ca href=\"https://www.shrinemaiden.org/forum/index.php/topic,18989.msg1286680.html#msg1286680\"\u003eheavy and uncritical advertising\u003c/a\u003e that is mainly responsible for elevating the Romantique Tp recordings to their current reference status within the community, especially if at least half of the alleged superiority of real hardware is founded on undefined behavior that can easily be fixed in the MIDI files themselves if people only bothered to look.\n\u003c/p\u003e\u003cp\u003e\n\tHere's where I stand: MIDI files are digital sheet music first and foremost, not an inferior version of tracker modules where the samples are sold separately. As such, the specific synth a MIDI file was written for is merely a secondary property of the composition – and even more so if the MIDI file contains little to nothing in terms of sound design and mostly restricts itself to the basic feature set of General MIDI. In turn, synth quirks and bugs are not a defined part of the composition either, unless they are clearly annotated and documented in the file itself. And most importantly: If the MIDI file specifies a certain timing and a recording fails to reproduce that timing, then that recording is not an accurate representation of the MIDI file.\u003cbr\u003e\n\tIn that regard, Sound Canvas VA is not only \u003cq\u003ethe closest alternative to the real thing\u003c/q\u003e, as a few people in the MIDI and retrogaming scene do have to admit, but \u003ci\u003esuperior\u003c/i\u003e to the real thing. I'll gladly take clarity and perfect timing accuracy in exchange for minor differences in effects, especially if the MIDI file does not explicitly and correctly define said effects to begin with. If I want a panning delay as part of the reverb, I add the respective \u003ci\u003eand correct\u003c/i\u003e SysEx message to define one – and if I don't, I \u003ci\u003edo not care about the reverb\u003c/i\u003e. You might still \u003ci\u003eget\u003c/i\u003e a panning delay on a certain synth, and you might even prefer how it sounds, but it's ultimately a rendering artifact and not a consciously intended part of the composition. In that way, it's similar to the individual flavor a musician adds to a performance of a piece of classical music.\u003cbr\u003e\n\tAnd as far as the differences in frequency response and resonant filters are concerned: In Yamaha land, these are exactly the main distinguishing factors between vintage WF-192XG sound cards (resembling the real SC-88Pro in these characteristics) and the \u003ca href=\"https://veg.by/en/projects/syxg50/\"\u003eS-YXG50 softsynth\u003c/a\u003e (resembling SCVA). Once I found out about that softsynth and how much clearer it sounded in comparison, I sold that old PCI sound card soon after.\n\u003c/p\u003e\u003cp\u003e\n\tIn the interest of preservation though, there's still one more unexplored solution that could be the ideal middle ground between the two approaches:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003ePlay the MIDIs through a real-hardware SC-88Pro again\u003c/li\u003e\n\t\u003cli\u003eCapture the actually observed system-exclusive settings that fall within the synth's supported and documented ranges\u003c/li\u003e\n\t\u003cli\u003eInsert them back into the MIDI file, creating a new bugfixed version\u003c/li\u003e\n\t\u003cli\u003eRe-record that bugfixed version through Sound Canvas VA\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\t\u003cstrong\u003eEdit (2024-03-10):\u003c/strong\u003e And since Romantique Tp has confirmed what exactly happens on real hardware, I'm going to do exactly that. These bugfixed Sound Canvas VA renderings will be a free bonus of the single next Shuusou Gyoku push, and will add another angle to the preservation of these soundtracks. In the meantime though, the Sound Canvas VA packs will sound like they do in the preview videos above.\n\u003c/p\u003e\u003cp\u003e\n\tOr, you know… Maybe none of this actually matters. \u003ca href=\"https://www.youtube.com/watch?v=W7fK540Fkk0\u0026t=712s\"\u003eHere's beatMARIO streaming some Shuusou Gyoku gameplay using what looks like a real-hardware SC-8850\u003c/a\u003e, which plays these MIDIs with occasionally noticeably different instrument patches and \u003ca href=\"https://www.youtube.com/watch?v=W7fK540Fkk0\u0026t=4360s\"\u003eno panning delay in the name registration theme\u003c/a\u003e, and he still enjoyed every second of it. Imagine undefined SysEx behavior not even being consistent within the same \u003ci\u003efamily\u003c/i\u003e of Roland synths… nah, I'm done arguing, let's get back to the actual work and cut some loops.\n\u003c/p\u003e\u003chr id=\"mly-2024-03-09\"\u003e\u003cp\u003e\n\tJust to be clear: I'm not suggesting that Romantique Tp should have been the one to cut their recordings into loops, or even just the one who defined where the loop points are supposed to be. On the surface, this seems to be a non-issue, and you'd just pick a point wherever each track \u003ci\u003eappears\u003c/i\u003e to loop, right? But with 39 MIDIs to cut and all the financial support from Ember2528, it made sense to also solve this problem more thoroughly, and algorithmically detect provably correct loop points for all of these files. Who knows, maybe we even find some surprises that make it all worth it?\u003cbr\u003e\n\tThis is the algorithm I came up with:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAt a basic level, we loop over the list of MIDI events and return the earliest and longest subrange that is immediately followed by an identical copy.\u003c/li\u003e\n\t\u003cli\u003eMIDI players, however, need loop point definitions that use MIDI pulse units rather than event list indices. This is especially necessary for \u003ca href=\"https://en.wikipedia.org/w/index.php?title=MIDI\u0026oldid=1199792271#Standard_files\"\u003emulti-track/SMF Type 1 sequences\u003c/a\u003e, which would otherwise require one loop start/end index pair per track, and then it \u003ci\u003estill\u003c/i\u003e wouldn't work because some of the tracks might not even have an event at the loop start/end point. This requires the detection algorithm and the player to agree on how to map event indices to time points and back, and simply going for the first event of each pulse (i.e., any event with a nonzero delta time) makes the most sense here. In turn, we can skip any potential start or end events that have a delta time of 0, speeding up the algorithm significantly for typical compositions with a high degree of polyphony.\u003c/li\u003e\n\t\u003cli\u003eNaively considering just the raw MIDI events works for MIDI playback. But as soon as we want to cut a recording based on the detected loop points, we need to account for the fact that MIDI playback is inherently stateful. Each of the 16 channels \u003cspan class=\"hovertext\" title=\"Yes, even MIDI 1.0 specified a way to have more than 16 channels in a multi-track/SMF Type 1 container. Look up FF 21 01 and FF 09.\"\u003eat the protocol level\u003c/span\u003e features at least the 128 \u003ci\u003econtinuous controllers\u003c/i\u003e (CCs) with a 7-bit state, the 14-bit pitch bend controller, and the 7-bit instrument program value, in addition to the global tempo of the piece. As a result, two ranges of events might look identical, but can still \u003ci\u003esound\u003c/i\u003e differently if the events before the first range changed one piece of state which is then only touched again near the end of that range. This requires us to track the full MIDI state at both the start and end of a loop, and reject any potential loop that differs in these states:\u003cfigure\u003e\n\t\t\u003cpre\u003eDelta\tPulse\t  Beat\tEvent\n   +0\t 1200\t 2:240\tController { CC 7, value  50 }\n\n+240\t 1440\t 3:000\tNoteOn { Key 60 }\n+240\t 1680\t 3:240\tNoteOn { Key 65 }\n+240\t 1920\t 4:000\tNoteOn { Key 67 }\n+240\t 2160\t 4:240\tNoteOn { Key 70 }\n+240\t 2400\t 5:000\tController { CC 7, value 100 }\n  +0\t 2400\t 5:000\tNoteOn { Key 67 }\n+240\t 2640\t 5:240\tNoteOn { Key 65 }\n\n+240\t 2880\t 6:000\tNoteOn { Key 60 }\n+240\t 3120\t 6:240\tNoteOn { Key 65 }\n+240\t 3360\t 7:000\tNoteOn { Key 67 }\n+240\t 3600\t 7:240\tNoteOn { Key 70 }\n+240\t 3840\t 8:000\tController { CC 7, value 100 }\n  +0\t 3840\t 8:000\tNoteOn { Key 67 }\n+240\t 4080\t 8:240\tNoteOn { Key 65 }\n\n+240\t 4320\t 9:000\tNoteOn { Key 60 }\n+240\t 4560\t 9:240\tNoteOn { Key 65 }\n+240\t 4800\t10:000\tNoteOn { Key 67 }\n+240\t 5040\t10:240\tNoteOn { Key 70 }\n+240\t 5280\t11:000\tController { CC 7, value 100 }\n  +0\t 5280\t11:000\tNoteOn { Key 67 }\n+240\t 5520\t11:240\tNoteOn { Key 65 }\u003c/pre\u003e\u003cfigcaption\u003e\n\t\tIn this example, a naive event-level scan would detect a loop between beats 3 and 6 as the same events are immediately repeated between beats 6 and 9. However, the piece starts with the first four notes at a channel volume of 50, which is only set to its later value of 100 on beat 5. Therefore, the actual loop ranges from beat 5 to 8. In turn, the piece needed to be at least 11 beats long to include the full second copy of the looped events and prove the loop as such.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThis check can be a bit too strict in some cases, though. A channel might start with one of its CCs at a specific value but then change the same CC to a different value at a later point before playing the first note. In such a case, the detected loop would be delayed to the second CC change even though the initial CC value has no impact on the sound. By filtering these redundant CC changes, we get to move the loop start point of a few tracks (original \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Stage 6 Boss/VIVIT-captured-'s first theme\"\u003e夢機械　～ Innocent Power\u003c/span\u003e and arranged \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Stage 5 Boss/Erich's theme\"\u003e魔法少女十字軍\u003c/span\u003e) back by a few seconds, to the position you'd expect.\u003c/li\u003e\n\t\u003cli\u003eFinally, we reject any overlong loops that themselves fully consist of multiple successive copies of the first N events.\u003cbr\u003e\n\tShuusou Gyoku's original MIDI files hide the original game's lack of MIDI looping by simply duplicating the looping sections enough times so that a typical player won't notice. The algorithm we have so far, however, would return a much longer loop if a MIDI file contains more than three successive copies of a looping section. The original version of \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Ending theme\"\u003eハーセルヴズ\u003c/span\u003e in particular repeats its 8 looping bars a total of 15 times before the MIDI ends, and this condition is necessary to detect the actual 8-bar loop instead of a 56-bar one.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOf course, this algorithm isn't perfect and won't work for every MIDI file out there. It doesn't consider things like differently ordered events within the same MIDI pulse, \u003ca href=\"https://en.wikipedia.org/wiki/NRPN\"\u003e(non-)registered parameter numbers\u003c/a\u003e, or the effect that SysEx messages can have on the state of individual channels. The latter would require the general SysEx decoding logic that I would have liked to have for the research above… actually, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/mly/issues/1\"\u003elet's add an issue\u003c/a\u003e and add the project to the order form. I'd really like to see a comprehensive open-source cross-vendor SysEx decoder library in my lifetime.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the implementation, I was happy to write some Rust again for a change, as it's a great fit for these standalone greenfield command-line tools that don't have to directly interact with the legacy C++ code bases that this project usually deals with. It's even better if the foundational functionality is not just available in \u003ci\u003ea\u003c/i\u003e crate, but \u003ca href=\"https://github.com/kovaxis/midly/tree/5b1300c6c0a634031e03589a44afb0c6cda639a8?tab=readme-ov-file#speed\"\u003ein four, with the community already having gone through multiple iterations to arrive at a tried and tested winner\u003c/a\u003e. Who knows, maybe I even get to rewrite this website in it one day? Just for the sheer meme value of doing so, of course.\u003cbr\u003e\n\tI also enjoyed this a lot from a technical point of view:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eYou might think that Rust's typical safety guarantees don't matter for the problem at hand. But then you accidentally write \u003ccode\u003e-=\u003c/code\u003e instead of \u003ccode\u003e+=\u003c/code\u003e for a \u003ccode\u003eu32\u003c/code\u003e that starts out at 0, and Rust immediately panics instead of silently underflowing to \u003ccode\u003eu32::MAX\u003c/code\u003e. This must have saved me at least 5 minutes of debugging the resulting logic error.\u003c/li\u003e\n\t\u003cli\u003eAs it turns out, my loop detection algorithm is \u003ca href=\"https://en.wikipedia.org/wiki/Embarrassingly%20parallel\"\u003eembarrassingly parallel\u003c/a\u003e. You might initially \u003ci\u003ethink\u003c/i\u003e about it in a sequential way because we always want the \u003ci\u003eearliest\u003c/i\u003e occurrence of the longest repeating section of MIDI events, which means that each new loop candidate further into the track has to be longer than the previous one. But since we always iterate over the entire MIDI, it makes perfect sense to divide and conquer the problem. Let's split the list of possible loop end points into equal chunks, scan them all in parallel for the earliest and longest loop within that chunk, and then pick the earliest and longest loop among \u003ci\u003ethose\u003c/i\u003e intermediate results as the final one. In Rust, you don't even have to think much about the chunks, as all of that can be easily done by replacing the iteration with \u003ca href=\"https://docs.rs/rayon/1.8.0/rayon/iter/trait.ParallelIterator.html#method.fold\"\u003eRayon's parallel fold\u003c/a\u003e and adding a \u003ccode\u003ereduce()\u003c/code\u003e with the same condition for the final step. This sped up the algorithm by \u003ci\u003eexactly\u003c/i\u003e the number of cores in my system.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis algorithm works well for the long MIDI files of Shuusou Gyoku's OST that all contain multiple duplicates of their loop section, but it quickly reaches its limit with the AST. Following the classic two-loop + fade-out format, that soundtrack was meant to be played back in generic MIDI players, and not to actually be put back into the game in looped form. Since the loop algorithm did, in fact, find inconsistencies even in the OST, two copies of the \u003ci\u003eapparent\u003c/i\u003e loop are sometimes not enough to prove cases where the \u003ci\u003eactual\u003c/i\u003e loop ends much later than you think it does. In a few cases, it would be enough to simply remove all volume change events from the fade-out to prove the actual loop, but in others, the algorithm would need MIDI event data far past the end of the fade-out.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, just giving up and not looping any of these tracks would be equally unfortunate. So how about shifting the question, from \u003cq\u003ewhat's the best loop in this MIDI file\u003c/q\u003e to \u003cq\u003ewhat's the best loop if the MIDI didn't fade out and instead repeated its apparent second loop a third time\u003c/q\u003e? As long as the detected loop in such a pre-processed file ends before the repeated range, it's still a valid loop in terms of the unmodified original.\u003cbr\u003e\n\tIdeally, we want to do this pre-processing programmatically with the same Rust library instead of manually editing the MIDI. Many sequencers (and \u003ci\u003eespecially\u003c/i\u003e XGworks) apply significant changes to a MIDI file's internal structure when saving its internal representation back to a MIDI file, which might even mess with our loop algorithm. So it would be very nice to have a more trustworthy tool that applies only the edit we actually want, and perfectly retains the rest of the MIDI.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's how this sub-project turned into a small suite of command-line MIDI operations in the classic Unix filter/pipeline style: Each command reads a MIDI file from \u003ccode\u003estdin\u003c/code\u003e, transforms it, and outputs text or the resulting MIDI file on \u003ccode\u003estdout\u003c/code\u003e. This way, we gain maximum transparency and reproducibility  as I can document the unique pre-processing steps for each AST track by \u003ca href=\"https://github.com/nmlgc/ssg/blob/P0275/GIAN07/LOADER.CPP#L31-L165\"\u003esimply providing the command lines\u003c/a\u003e. And sure, we're re-encoding and re-decoding the full MIDI sequence at every step along such a pipeline, but \u003ca href=\"https://computers-are-fast.github.io/\"\u003ecomputers are fast\u003c/a\u003e, Rust and the midly library in particular are ⚡\u0026nbsp;blazingly fast\u0026nbsp;⚡, and the usability benefits of this pipeline model far outweigh any theoretical performance drops.\u003cbr\u003e\n\tHere's the full list of commands that made it into the resulting \u003ccode\u003emly\u003c/code\u003e tool:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ckbd\u003ecut\u003c/kbd\u003e: Extremely basic removal of MIDI events within a certain range.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003edump\u003c/kbd\u003e: Dumps all MIDI events into a textual table. All event lists in this blog post are based on this output.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003eduration\u003c/kbd\u003e: Shows the duration of a MIDI file in pulses, beats, seconds, and PCM samples.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003efilter-note\u003c/kbd\u003e: Removes all Note On events within a certain range, retaining all other events. This allows us to generate separate intro and loop MIDIs, whose renderings we can then splice back into a single loopable waveform with no discontinuities, which is not guaranteed when rendering a single MIDI file. This provides the last missing piece needed for rendering perfect, sample-accurate loops through Sound Canvas VA.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003eloop-find\u003c/kbd\u003e: The loop detection algorithm described above.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003eloop-unfold\u003c/kbd\u003e: Duplicates MIDI events from a given point to the end of the track. A budget solution for the problem of creating synthetic loops – arbitrary copying of arbitrary subranges to arbitrary destinations would have been undeniably nicer, but also much more complex, and I didn't need that full flexibility for the task at hand.\u003c/li\u003e\n\t\u003cli\u003e\u003ckbd\u003esmf0\u003c/kbd\u003e: Flattening multi-track/SMF Type 1 MIDI sequences into single-track/SMF Type 0 ones. Having this conversion as a distinct operation in our toolset allows other operations to exclusively support SMF Type 0 if a Type 1 implementation would either take significant additional effort or just duplicate the Type 0 flattening algorithm. This group of operations includes \u003ckbd\u003eloop-find\u003c/kbd\u003e, \u003ckbd\u003ecut\u003c/kbd\u003e, and even the real-time output for \u003ckbd\u003eduration\u003c/kbd\u003e because tempo events can theoretically occur on any track.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis feature set should strike a good balance between not spending \u003ci\u003etoo\u003c/i\u003e much of the Shuusou Gyoku budget on tangential problems, but still offering a decent solution for the problem at hand. As a counterexample, the obvious killer feature – deserializing a \u003ccode\u003edump\u003c/code\u003e back into a Standard MIDI File – would have gone way past the budget. While \u003ca href=\"https://docs.rs/parse-display/latest/parse_display/\"\u003ethere are crates that free you from the need to write manual parsing code for basic data structures\u003c/a\u003e, they would instead require \u003ci\u003ea lot\u003c/i\u003e of attribute boilerplate – and if the library that provided the structures doesn't already come with these attributes, you now have to duplicate all the structures, and convert back and forth between the original structures and your copies. Not to mention that we'd still have to write code for the high-level structure of the \u003ccode\u003edump\u003c/code\u003e output…\n\u003c/p\u003e\u003cp\u003e\n\tIf we put it all together, this is what we can do:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e$ \u0026lt;ssg_02.mid mly loop-find\nBest loop in note space: 4 events (between event #[117, 121[ and [121, 125[)\nFirst note: event    71 / pulse    960 / beat   2:000 / 0:00:800m\nLoop start: event   117 / pulse   1680 / beat   3:240 / 0:01:400m\n  Loop end: event   121 / pulse   1920 / beat   4:000 / 0:01:600m\n\n$ \u0026lt;ssg_02.mid mly cut 466: | mly loop-unfold 240: | mly -r 44100 loop-find\nTrack #0: Removing events #[16439, 19881[\nTrack #0: Repeating events #[8344, 16439[ at the end of the sequence\nBest loop in note space: 8095 events (between event #[5625, 13720[ and [13720, 21815[)\nFirst note: event    71 / pulse    960 / beat   2:000 / 0:00:800m\nLoop start: event  5625 / pulse  75361 / beat 157:001 / 1:03:531m\n  Loop end: event 13720 / pulse 183841 / beat 383:001 / 2:34:726m\n\nBest loop in recording space:  8095 events (between event #[5709, 13804[ and [13804, 21899[)\nFirst note: event    71 / pulse    960 / beat   2:000 / 0:00:800m / sample    35280.00\nLoop start: event  5709 / pulse  77280 / beat 161:000 / 1:05:163m / sample  2873667.66\n  Loop end: event 13804 / pulse 185760 / beat 387:000 / 2:36:358m / sample  6895375.27\u003c/pre\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTranslation:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe best loop found in the raw MIDI file spans 4 events and 200 milliseconds. Clearly, this is not the loop we're looking for.\u003c/li\u003e\n\t\u003cli\u003eLet's \u003ckbd\u003ecut\u003c/kbd\u003e off all events from the start of the fade-out to the end, do a \u003ckbd\u003eloop-unfold\u003c/kbd\u003e copy of all events from the position during the apparent second loop that corresponds to where the fade-out started, and try looking for a loop in that modified MIDI.\u003c/li\u003e\n\t\u003cli\u003eThe resulting loop is 1:31m long, which is exactly what we were hoping to find.\u003cul\u003e\n\t\t\u003cli\u003eThe \u003ci\u003enote space\u003c/i\u003e loop represents the earliest possible event range with equivalent per-channel controller and pitch bend state at both ends. This loop is only appropriate for MIDI players, as its bounds can fall into the middle of notes that are played with a different channel state at the start and end of the loop. This is why it doesn't show any sample positions.\u003c/li\u003e\n\t\t\u003cli\u003eThe \u003ci\u003erecording space\u003c/i\u003e loop ensures that this doesn't happen. It's also always placed on a Note On event with non-zero velocity, which eases the splicing of separate \u003ccode\u003efilter-note\u003c/code\u003e recordings. This way, it's enough to remove leading silence from the loop part and mix it exactly at the indicated sample position.\u003c/li\u003e\n\t\t\u003cli\u003eThe detected loop is also nowhere close to the cut point at beat 466, matching our condition for validity. All events within the loop came from ZUN's original composition, and the \u003ckbd\u003ecut\u003c/kbd\u003e/\u003ckbd\u003eloop-unfold\u003c/kbd\u003e combo merely provided the remaining 63% of events necessary to prove this loop as such.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"midiviz-2024-03-09\"\u003e\u003cp\u003e\n\tSo, where are these loop quirks that justify why some of these audio files are longer than you'd think they should be? Just listing them as text wouldn't really communicate just how minor these are. It would be much nicer to visualize them in a way that highlights the exact inconsistencies within a fixed range of MIDI measures. Screenshots of MIDI sequencer or DAW windows won't capture these aspects all too well because these programs are geared toward fine-grained editing of single tracks, not visualization of details across all channels.\n\u003c/p\u003e\u003cfigure class=\"fullres\"\u003e\n\t\u003cimg\n\t\tsrc=\"/blog/static/2024-03-09-SH01-o02-REAPER.png?fa4d74bb\"\n\t\talt=\"Screenshot of the first 8 measures of Shuusou Gyoku's Stage 1 theme (フォルスストロベリー) in its OST version, as visualized by REAPER's piano roll\"\n\t\tstyle=\"max-height: unset;\"\n\t\u003e\n\t\u003cfigcaption\u003eREAPER's piano roll nicely snaps to a certain range, but good luck picking out the individual lines from the single volume lane at the bottom of the screen, or spotting a 7-point difference. Not to mention that CC\u0026nbsp;#11 (Expression) makes up an equal part of a channel's final perceived volume, which is the metric we'd \u003ci\u003eactually\u003c/i\u003e want to visualize.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTypical MIDI visualizers, however, are on the complete opposite end of the spectrum. In recent years, \u003cq\u003eMIDI visualization\u003c/q\u003e has become synonymous with the typical Synthesia style of YouTube videos with a big keyboard at the bottom, note bars flying in from the top, and optional fancy effects once those notes hit the top of the keyboard. The Black MIDI community has been churning out \u003ca href=\"https://hans5958.github.io/Black-MIDI-Meta/links/\"\u003etons of identically looking MIDI visualizers\u003c/a\u003e in recent years that mainly seem to differ in the programming language they're written in, and in how well they can cope with the blackest of black MIDIs.\u003cbr\u003e\n\tThankfully, most of these visualizers are open-source and have small and manageable codebases. \u003ca href=\"https://github.com/kosua20/MIDIVisualizer\"\u003eThe project with the most GitHub stars and the most generic name\u003c/a\u003e seemed to be the best starting point for hacking in the missing features, despite using GLSL shaders which I had no prior experience with. It was long overdue that I did something with GLSL though – it added a nice educational aspect to these hacks, and it still was easier than \u003ca href=\"https://github.com/arduano/wasabi\"\u003edeciphering whatever the fastest and hyper-optimized Rust visualizer is doing\u003c/a\u003e.\u003cbr\u003e\n\tStill, this visualizer needed a total of 18 small features and bugfixes to be actually usable for demonstrating Shuusou Gyoku's loop quirks. As such, these hacks turned into yet another tangential sub-project that could have easily consumed another two pushes if I cleaned up the code and published the result. But that would have \u003ci\u003ereally\u003c/i\u003e gone way past the budget for something that people might not even care about. So here's what we're going to do:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eI've added this \u003ci\u003eMIDI visualizer\u003c/i\u003e as a new goal to the order form. This goal is eligible for microtransactions, so you don't have to fund a full push to see the first changes committed and released.\u003c/li\u003e\n\t\u003cli\u003eThe upstream project seems to have been abandoned recently, which is the perfect excuse for not even trying to merge in my sweeping changes with a series of pull requests. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e The code sure needs a lot of cleanup and deduplication, and \u003ci\u003eespecially\u003c/i\u003e a more build system-friendly way of embedding its shader source code.\u003c/li\u003e\n\t\u003cli\u003eEvery backer who supports this goal with at least 0.1 pushes or microtransactions will get a Windows binary with my current hacked-in changes as a preview, immediately after the purchase. Shoutout to the MIT license for letting me do this 😛\u003cbr\u003e\n\tAs usual, once the code is done, the final cleaned-up version will be available for free for everyone, in both source code and binary release form.\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"loopquirks-2024-03-09\"\u003e\u003cp\u003e\n\tAlright then! Here's how to read the visualizations:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe transparency of each note represents its velocity multiplied by the channel volume and expression. To spot volume inconsistencies, you'd compare the opacity of equivalent notes in the two ranges.\u003c/li\u003e\n\t\u003cli\u003eThe X-axis of these visualizations uses linear/real time, so the width of each measure represents the exact time it takes to be played relative to the other measures in the visualized range. To spot tempo inconsistencies, you'd compare the distance between the bar lines.\u003c/li\u003e\n\t\u003cli\u003eNotes that are duplicated on two or more channels may be colored differently in the loop start and end views. These are rendering order inconsistencies and don't communicate anything about the MIDI.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cul class=\"loopquirks-2024-03-09\"\u003e\n\t\u003cli\u003e\n\t\t\u003ci\u003eStage 1 theme (\u003cspan lang=\"ja\"\u003eフォルスストロベリー\u003c/span\u003e), original and arranged version\u003c/i\u003e: The string and harmonica channels are slightly louder on the apparent first loop than on the others.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:31m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e1:04m – 2:34m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-o02.hd.webp?f8e078df\" preload=\"none\" controls data-title=\"OST version\" loop data-active width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"386\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o02.hd.avi?4c03dd14\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-o02.hd.webm?828da16d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-o02.hd.webm?7e401026\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the OST version of the Stage 1 theme (フォルスストロベリー). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o02.hd.avi?4c03dd14\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"97\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"193\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"290\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a02.hd.webp?437540cb\" preload=\"none\" controls data-title=\"AST version\" loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"392\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a02.hd.avi?dd609dff\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a02.hd.webm?a3745b51\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a02.hd.webm?0bb64421\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of the Stage 1 theme (フォルスストロベリー). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a02.hd.avi?dd609dff\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"98\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"196\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"294\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eMei and Mai's theme (\u003cspan lang=\"ja\"\u003eディザストラスジェミニ\u003c/span\u003e), arranged version\u003c/i\u003e: The one and only quirk that's caused by different notes – the first loop has an E♭ on the slap bass channel in measure 32, but the second loop has a G♭ in the corresponding measure 72.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:02m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e0:50m – 1:51m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a05.hd.webp?6aafd4df\" preload=\"none\" controls loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"368\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a05.hd.avi?0a53912c\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a05.hd.webm?7ed5048d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a05.hd.webm?c73b4522\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of Mei and Mai's theme (ディザストラスジェミニ). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a05.hd.avi?0a53912c\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"92\" data-title=\"E♭\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"184\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"276\" data-title=\"G♭\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eStage 3 theme (\u003cspan lang=\"ja\"\u003e華の幻想\u0026nbsp;\u0026nbsp;紅夢の宙\u003c/span\u003e), original and arranged version\u003c/i\u003e:\n\t\tThe trumpet channel starts out panned to the center of the stereo field (64), before being left-panned by 25% (48) at 1:04m, where it stays for the rest of the track.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:29m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e1:04m – 2:32m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-o06.hd.webp?19a0d9c8\" preload=\"none\" controls data-title=\"OST version\" loop data-active width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"948\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o06.hd.avi?0fafbf23\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-o06.hd.webm?895ccdf2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-o06.hd.webm?e5874ec5\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the OST version of the Stage 3 theme (華の幻想\u0026nbsp;\u0026nbsp;紅夢の宙). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o06.hd.avi?0fafbf23\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"285\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"474\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"759\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a06.hd.webp?d450d26f\" preload=\"none\" controls data-title=\"AST version\" loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"948\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a06.hd.avi?cf5bff9f\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a06.hd.webm?8a5aa0b1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a06.hd.webm?a49fe481\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of the Stage 3 theme (華の幻想\u0026nbsp;\u0026nbsp;紅夢の宙). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a06.hd.avi?cf5bff9f\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"285\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"474\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"759\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\t\u003cfigcaption\u003e\n\t\t\t\tI didn't come up with a good way of visualizing panning in a 2D plane, so you have to trust your ears with this one.\n\t\t\t\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eMarie's theme (\u003cspan lang=\"ja\"\u003e機械サーカス　～ Reverie\u003c/span\u003e), arranged version\u003c/i\u003e: Every apparent loop modulates up by a semitone 16 measures before it ends, and remains in that new key at the start of the next loop, so the piece technically doesn't loop at all. The original stays in G♯m throughout.\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a09.hd.webp?3cd491ab\" preload=\"none\" controls loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"1556\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a09.hd.avi?2cc6eda8\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a09.hd.webm?40bac2fd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a09.hd.webm?a794c06d\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of Marie's theme (機械サーカス　～ Reverie). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a09.hd.avi?2cc6eda8\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"189\" data-title=\"G♯m\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"582\" data-title=\"Piece repeats\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"968\" data-title=\"Am\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1361\" data-title=\"Piece repeats\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eStage 5 theme (\u003cspan lang=\"ja\"\u003eカナベラルの夢幻少女\u003c/span\u003e), original version\u003c/i\u003e: The ritardando near the supposed end of the first loop drops from 145 BPM to 118 BPM, but only to 129 BPM in all further loops.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:39m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e1:33m – 3:11m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\tYup, that means that the intro part technically makes almost up the entire apparent loop. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e ZUN replaced the ritardando with instant tempo changes in the arranged version, which moves the loop to its expected place at the start of the track.\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\t\t\tThe loop start and end points are in the respective next measure past this range.\n\t\t\t\u003c/div\u003e\u003cdiv\u003e\u003c/div\u003e\u003c/figcaption\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-o10.hd.webp?0374f9d9\" preload=\"none\" controls data-title=\"OST version\" loop data-active width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"408\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o10.hd.avi?894f9092\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-o10.hd.webm?fbfae7ab\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-o10.hd.webm?4df254e3\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the OST version of the Stage 5 theme (カナベラルの夢幻少女). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o10.hd.avi?894f9092\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"205\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a10.hd.webp?58595332\" preload=\"none\" controls data-title=\"AST version\" loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"440\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a10.hd.avi?044da9c5\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a10.hd.webm?d5a1e646\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a10.hd.webm?680317ae\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of the Stage 5 theme (カナベラルの夢幻少女). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a10.hd.avi?044da9c5\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"220\" data-title=\"Fixed quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eStage 6 theme (\u003cspan lang=\"ja\"\u003eアンティークテラー\u003c/span\u003e), arranged version\u003c/i\u003e: The string channel starts out with the maximum expression of 127, but then only goes up to 120 after some fading notes later in the piece, where it stays for the beginning of the second loop.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:53m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e0:13m – 2:05m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003cfigcaption\u003eSame here.\u003c/figcaption\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a12.hd.webp?a03bc0e0\" preload=\"none\" controls loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"372\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a12.hd.avi?58ab45c9\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a12.hd.webm?e5b87d99\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a12.hd.webm?deea53b5\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of the Stage 6 theme (アンティークテラー). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a12.hd.avi?58ab45c9\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli id=\"lq-a13-2024-03-09\"\u003e\n\t\t\u003ci\u003eVIVIT-captured-'s first theme (\u003cspan lang=\"ja\"\u003e夢機械　～ Innocent Power\u003c/span\u003e), arranged version\u003c/i\u003e: Has a unique ending section that starts in Gm and then modulates through Em and Fm before it fades out on F♯m.\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a13.hd.webp?daecdcf5\" preload=\"none\" controls loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"4000\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a13.hd.avi?e0d18df3\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a13.hd.webm?37de88dc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a13.hd.webm?416c1488\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of VIVIT-captured-'s first theme (夢機械　～ Innocent Power). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a13.hd.avi?e0d18df3\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"800\" data-title=\"Gm\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1600\" data-title=\"Em\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"2400\" data-title=\"Fm\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"3200\" data-title=\"F♯m\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eVIVIT-captured-'s second theme (\u003cspan lang=\"ja\"\u003e幻想科学 ～ Doll's Phantom\u003c/span\u003e), original and arranged version\u003c/i\u003e: Another fade-related 127 vs. 120 expression inconsistency, this time on the orange square channel.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\u003cdd\u003e0:01m – 1:32m\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\u003cdd\u003e1:03m – 2:34m\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-o14.hd.webp?b9fd57b9\" preload=\"none\" controls data-title=\"OST version\" loop data-active width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"385\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o14.hd.avi?a3849a65\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-o14.hd.webm?d723fffa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-o14.hd.webm?9238cfde\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the OST version of VIVIT-captured-'s second theme (幻想科学 ～ Doll's Phantom). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o14.hd.avi?a3849a65\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"90\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"193\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"283\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a14.hd.webp?e1c47c5f\" preload=\"none\" controls data-title=\"AST version\" loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"385\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a14.hd.avi?6d8f7b10\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a14.hd.webm?ea252d1e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a14.hd.webm?590410cd\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of VIVIT-captured-'s second theme (幻想科学 ～ Doll's Phantom). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a14.hd.avi?6d8f7b10\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"90\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"193\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"283\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003ci\u003eVIVIT-captured-'s third theme (\u003cspan lang=\"ja\"\u003e少女神性　～ Pandora's Box\u003c/span\u003e), original and arranged version\u003c/i\u003e: Another tempo inconsistency: A slightly differently shaped ritardando before the bell tree hit in the supposed first loop.\n\t\t\u003cdl\u003e\n\t\t\t\u003cdt\u003eApparent loop:\u003c/dt\u003e\n\t\t\t\u003cdd\u003e0:40m\u0026nbsp;–\u0026nbsp;1:57m\u0026nbsp;(original),\n\t\t\t\t0:41m\u0026nbsp;–\u0026nbsp;1:59m\u0026nbsp;(arranged)\u003c/dd\u003e\n\t\t\t\u003cdt\u003eActual loop:\u003c/dt\u003e\n\t\t\t\u003cdd\u003e1:17m\u0026nbsp;–\u0026nbsp;2:34m\u0026nbsp;(original),\n\t\t\t\t1:18m\u0026nbsp;–\u0026nbsp;2:37m\u0026nbsp;(arranged)\u003c/dd\u003e\n\t\t\u003c/dl\u003e\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-o15.hd.webp?cf61271a\" preload=\"none\" controls data-title=\"OST version\" loop data-active width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"731\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o15.hd.avi?0152ebb7\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-o15.hd.webm?ec966cc1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-o15.hd.webm?3bfd5588\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the OST version of VIVIT-captured-'s third theme (少女神性　～ Pandora's Box). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-o15.hd.avi?0152ebb7\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"166\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"365\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"532\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a15.hd.webp?8fe2771e\" preload=\"none\" controls data-title=\"AST version\" loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"746\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a15.hd.avi?60168e7a\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a15.hd.webm?f06d4313\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a15.hd.webm?58d1480d\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of VIVIT-captured-'s third theme (少女神性　～ Pandora's Box). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a15.hd.avi?60168e7a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"147\" data-title=\"Loop start\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"373\" data-title=\"Quirk\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"521\" data-title=\"Loop end\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli id=\"lq-a17-2024-03-09\"\u003e\n\t\t\u003ci\u003eMarisa's theme (\u003cspan lang=\"ja\"\u003e魔女達の舞踏会\u003c/span\u003e), arranged version\u003c/i\u003e: Has a unique 8-bar ending section that is first played in Cm and then loops in C♯m while fading out.\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a17.hd.webp?87a7eec0\" preload=\"none\" controls loop width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"3180\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a17.hd.avi?99c2e9d7\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a17.hd.webm?9ea13550\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a17.hd.webm?85a8897c\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of Marisa's theme (魔女達の舞踏会). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a17.hd.avi?99c2e9d7\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"797\" data-title=\"Cm\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1592\" data-title=\"C♯m, first\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"2387\" data-title=\"C♯m, second\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\u003cli id=\"lq-a19-2024-03-09\"\u003e\n\t\t\u003ci\u003eEnding theme (\u003cspan lang=\"ja\"\u003eハーセルヴズ\u003c/span\u003e), arranged version\u003c/i\u003e: Probably the best-known one out of these, and I'm talking of course about the beautiful ending section. I'm making the executive decision to not loop this track in-game, and letting it fade to silence instead.\n\t\t\u003cfigure class=\"viz-2024-03-09\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-loop-quirk-a19.hd.webp?fbc68fa6\" preload=\"none\" controls width=\"1280\" height=\"720\" data-fps=\"60\" data-frame-count=\"2366\" style=\"aspect-ratio: 1280 / 720\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a19.hd.avi?bd0ae90c\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-loop-quirk-a19.hd.webm?b6f2c499\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-loop-quirk-a19.hd.webm?2ae48e9c\" type=\"video/webm\"\u003eMIDI visualization of the loop quirk in the AST version of the ending theme (ハーセルヴズ). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-loop-quirk-a19.hd.avi?bd0ae90c\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"pbgmidi-2024-03-09\"\u003e\u003cp\u003e\n\tBefore we package up these looped soundtracks, let's take a quick look at how they would be shown off in the Music Room. The Seihou Music Rooms carry over the per-channel keyboards from TH05, add the current per-channel volume, expression, and pan pot values, and top it off with a fake spectrum analyzer. All of these visualizations rely on MIDI data, and the Music Room would feel very dull and boring without them. Just look at Kioh Gyoku, whose Music Room basically turns into a still image in \u003cspan lang=\"ja\"\u003eWAVE\u003c/span\u003e mode.\u003cbr\u003e\n\tRetaining these visualizations even when playing waveform BGM was very important for me, and not just because it would make for a unique high-quality feature that would break new ground. It can also double as proof that the waveform versions are, in fact, in perfect sync with both the MIDIs they are based on, and, by extension, the respective stage scripts.\u003cbr\u003e\n\tHowever, this would require the game to process the MIDIs and update the internal visualization state without simultaneously playing them back through the WinMM\u0026nbsp;/ MME\u0026nbsp;/ \u003ccode\u003emidiOut*()\u003c/code\u003e API. And just like graphics and text rendering, Shuusou Gyoku's original code came with zero architectural separation between platform-independent processing logic and platform-specific playback…\n\u003c/p\u003e\u003cp\u003e\n\tSo I accidentally rewrote almost the entire MIDI code to achieve said separation. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e This also provided a great occasion to modernize this code and add some much-needed robustness for potential MIDI mods, while retaining the original code's approach of iterating over raw SMF byte streams. It might all have been very excessive for a delivery that was supposed to be just about waveform BGM support, but on the plus side, MIDI output is now portable to any other system's MIDI API as well.\n\u003c/p\u003e\u003cp\u003e\n\tSurprisingly though, it was Shuusou Gyoku's original MIDI timing that quickly turned out to be rather inaccurate, and not the waveforms. The exact numbers vary depending on the piece, but the game played back every MIDI about 1% slower than notated, adding about 2 or 3 seconds to their total playback time after 5 minutes. Tempo changes in particular were the biggest causes of desynchronizations with the waveforms… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tTo understand how this can happen to begin with, we have to look closer at how you're supposed to use the \u003ccode\u003emidiOut*()\u003c/code\u003e API. This API is as low-level as it gets, only covering the transmission of a single MIDI message to the selected output device \u003ci\u003eright now\u003c/i\u003e. There is no concept of note timing at this low level, so it's completely up to the program to parse delta times and tempo change events out of the MIDI file and correctly time the calls to this API for each MIDI message. With all the code that runs between the API and the actual renderer of the synth \u003ci\u003efor every single message\u003c/i\u003e, the resulting timing can only ever be an approximation of the MIDI file. This doesn't really matter for the timescales and polyphony levels of typical music because, again, \u003ca href=\"https://computers-are-fast.github.io/\"\u003ecomputers are fast\u003c/a\u003e, but such an API is fundamentally unsuitable for accurately playing back even just a moderately complex million-note Black MIDI. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tShuusou Gyoku handles this required manual timing in the simplest possible way: It runs a MIDI processing function (\u003ccode\u003eMid_Proc()\u003c/code\u003e in the code) at an interval of 10\u0026nbsp;ms, which processes and instantly sends out all MIDI events that have occurred at \u003ci\u003eany\u003c/i\u003e point within the last 10\u0026nbsp;ms, maintaining merely their order. This explains not only why the original game incremented its \u003ccode\u003eMIDI TIMER\u003c/code\u003e by multiples of 10, but also \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/8\"\u003ethe infamous missing drums when playing the soundtrack through the Microsoft GS Wavetable Synth\u003c/a\u003e:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eZUN reduced all drum notes to the minimum possible length allowed by the 480 PPQN pulse resolution of these MIDI files.\u003c/li\u003e\n\t\u003cli\u003eIn regular music notation, this corresponds to \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e1920\u003c/sub\u003eth notes.\u003c/li\u003e\n\t\u003cli\u003eWhile the exact real-time length in purely mathematical terms depends on the tempo of a piece, it only has to be ≥13 BPM for a \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e1920\u003c/sub\u003eth note to be shorter than 10\u0026nbsp;ms.\u003c/li\u003e\n\t\u003cli\u003eTherefore, the higher the BPM, the higher the chance that both a drum note's \u003ci\u003eNote On\u003c/i\u003e and \u003ci\u003eNote Off\u003c/i\u003e messages are sent within the same call to \u003ccode\u003eMid_Proc()\u003c/code\u003e, with the respective two \u003ccode\u003emidiOut*()\u003c/code\u003e API calls only being at best a two-digit number of \u003ci\u003emicro\u003c/i\u003eseconds apart.\u003c/li\u003e\n\t\u003cli\u003eSo it only makes sense why cheap MIDI synths that don't even respond to reverb or release time messages completely drop any note with such a short length. After all, at a sampling rate of 44,100\u0026nbsp;Hz, a note would have to be at least 22.7\u0026nbsp;µs long to be represented by even a single PCM sample.\u003c/li\u003e\n\t\u003cli\u003eThis also extends to the visualizations above, and was the reason why I chose to render all drum notes as fixed-size diamonds. Otherwise, they would barely be visible.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut while sending MIDI events in such quantized chunks might not be perfect, it can't be the cause behind multi-second playback slowdowns. Instead, this issue has to boil down to the way Shuusou Gyoku times each individual message, and specifically how it converts between MIDI pulse units and real-time (milli)seconds. pbg's original MIDI code chose to do this in an equally confusing and inaccurate way: it kept two counters that tracked the current MIDI pulse before and after the latest tempo change, used the value of the latter counter to decide which events to process, and only added the pulse equivalent of 10\u0026nbsp;ms to this counter at the end of \u003ccode\u003eMid_Proc()\u003c/code\u003e in the then current tempo. \u003ca href=\"https://github.com/nmlgc/ssg/commit/47c7f3ceb11b3674979da83488d09ec6b221f01e\"\u003eThe commit message for my rewritten algorithm details the problems with this approach using nice ASCII art in case you're interested\u003c/a\u003e, but in short, the main problem lies in how the single final addition can only consider a single tempo change within each call to \u003ccode\u003eMid_Proc()\u003c/code\u003e. If a MIDI file contains tempo ramps with less than 10\u0026nbsp;ms between each different tempo, the original game would only use the last of these tempo values as the basis for converting \u003ci\u003ethe entire\u003c/i\u003e 10\u0026nbsp;ms back into MIDI pulses. Not to mention that \u003ci\u003emaybe\u003c/i\u003e MIDI pulses aren't the best unit in a game that still \u003ca href=\"/blog/2023-09-30#utmath-2023-09-30\"\u003e📝 treats the FPU as lava\u003c/a\u003e and doesn't use any fixed-point means of increasing the resolution of the 10\u0026nbsp;ms→pulse division either…\n\u003c/p\u003e\u003cp\u003e\n\tOn the contrary, it's much more accurate to immediately convert every encountered MIDI delta time to a real-time quantity and use that unit for event timing, especially if we want to restrict ourselves to integer math. Signed 64-bit integers are enough to fit the product of the slowest possible MIDI tempo ((﻿2\u003csup\u003e24\u003c/sup\u003e\u0026nbsp;-\u0026nbsp;1﻿)\u0026nbsp;µs per quarter note) and the highest possible MIDI delta time (﻿2\u003csup\u003e28\u003c/sup\u003e\u0026nbsp;-\u0026nbsp;1﻿) at nanosecond precision (10\u003csup class=\"hovertext\" title=\"Tempo values already are in microseconds, so nanoseconds only add a factor of 1000 here.\"\u003e3\u003c/sup\u003e), with one bit to spare. Then, we arrive at a much simpler timing algorithm:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eEach simultaneously playing track gets a \u003ci\u003enext event\u003c/i\u003e timer, starting out at 0\u003c/li\u003e\n\t\u003cli\u003eWhen looking at the next event, add the converted nanosecond value of its delta time to this timer\u003c/li\u003e\n\t\u003cli\u003eSubtract the equivalent of 10\u0026nbsp;ms from each track's timer at the beginning of the processing function\u003c/li\u003e\n\t\u003cli\u003eAs long as the timer is ≤0, process and send the next message\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe additive nature of this timer not only naturally allows more than one event to happen within a single \u003ccode\u003eMid_Proc()\u003c/code\u003e call, but also averages out any minor timing inconsistencies across the length of a track.\n\u003c/p\u003e\u003cp\u003e\n\tThis new algorithm did improve the overall timing accuracy, but only barely, shaving off just ≈100\u0026nbsp;ms of the total duration. Turns out that the main source behind the slowness was hiding somewhere else entirely, in \u003ca href=\"https://github.com/pbghogehoge/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/DirectXUTYs/PBGMIDI.C#L597\"\u003ethe single line that deserializes tempo values from MIDI's big-endian representation into the native integer format\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre class=\"chroma\"\u003eassert(length_of_tempo_message == 3);\nuint32_t tempo = 0;\nfor(int i = 0; i \u0026lt; length_of_tempo_message; i++) {\n\u003cspan class=\"gd\"\u003e-\ttempo += ((tempo \u0026lt;\u0026lt; 8) + (*track_data++));\u003c/span\u003e\n\u003cspan class=\"gi\"\u003e+\ttempo  = ((tempo \u0026lt;\u0026lt; 8) + (*track_data++));\u003c/span\u003e\n}\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tYup – the original code performed two additions per byte, which incorrectly added the interim value at every byte to the final result, and yielded a tempo that is ≈0.8%\u0026nbsp;/ ≈1\u0026nbsp;BPM slower than notated in the MIDI file, matching the number we were looking for. That's why the \u003ccode\u003e|\u003c/code\u003e/\u003ccode\u003eOR\u003c/code\u003e operator is the safer one to use in such a bit-twiddling context…\u003cbr\u003e\n\tBut now I'm curious. This is such a tiny bug that is bound to remain unnoticed until someone compares the game's MIDI output to another renderer. It must have certainly made it into other games whose MIDI code is based on Shuusou Gyoku's, or that pbg was involved with. And sure enough, not only did this bug \u003ca href=\"https://github.com/pbghogehoge/kog/blob/7d45ba8ddc1b01987ccca416dbd7901a23f4412f/NewPBGLib/MIDI/MidiSub.cpp#L70\"\u003esurvive Kioh Gyoku's OOP refactoring\u003c/a\u003e, but it even traveled into Windows Touhou, where it remained in every single game that supported MIDI playback. Now we know for a fact that pbg's \u003ci\u003eProgram Support\u003c/i\u003e role in the TH06 credits involved sharing ready-made, finished code with ZUN:\n\u003c/p\u003e\n\u003cfigure class=\"side_by_side small\"\u003e\n\t\u003ca href=\"/blog/static/2024-03-09-TH06-MIDI-tempo-bug.png?3e988f05\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-TH06-MIDI-tempo-bug.png?3e988f05\"\n\t\t\talt=\"Disassembly of the Shuusou Gyoku MIDI tempo deserialization bug in TH06\"\n\t\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2024-03-09-TH07-MIDI-tempo-bug.png?879a70ef\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-TH07-MIDI-tempo-bug.png?879a70ef\"\n\t\t\talt=\"Disassembly of the Shuusou Gyoku MIDI tempo deserialization bug in TH07\"\n\t\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2024-03-09-TH08-MIDI-tempo-bug.png?37b30301\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-TH08-MIDI-tempo-bug.png?37b30301\"\n\t\t\talt=\"Disassembly of the Shuusou Gyoku MIDI tempo deserialization bug in TH08\"\n\t\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2024-03-09-TH09-MIDI-tempo-bug.png?df2c4454\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-TH09-MIDI-tempo-bug.png?df2c4454\"\n\t\t\talt=\"Disassembly of the Shuusou Gyoku MIDI tempo deserialization bug in TH09\"\n\t\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2024-03-09-TH10-MIDI-tempo-bug.png?098e9ecf\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-TH10-MIDI-tempo-bug.png?098e9ecf\"\n\t\t\talt=\"Disassembly of the Shuusou Gyoku MIDI tempo deserialization bug in TH10\"\n\t\t\u003e\u003c/a\u003e\n\t\u003cfigcaption\u003eThe broken tempo deserialization in the respective latest full versions of TH06 through TH10. And yes, that's TH10 – even though TH09's trial version was the last game to ship MIDI versions of its soundtrack, TH10 still contained all of pbg's MIDI code that originated back in Shuusou Gyoku, before TH11 finally removed it.\u003cbr\u003e\n\tAmusingly, ZUN's compiler even started optimizing the combination of left-shifting and addition to a multiplication with 257 for TH09, which even sort of highlights this bug if you're used to reading x86 ASM.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat leaves support for MIDI loop points as the only missing feature for syncing MIDI data with a looping waveform track. While it didn't require all \u003ci\u003etoo\u003c/i\u003e much code, pbg's original zero-copy approach of iterating over raw MIDI data definitely injected a lot of complexity into the required branches. Multi-track/SMF Type 1 files require quite a bit of extra thought to correctly calculate delta times across loop boundaries that reach past the end of the respective track, while still allowing the real-time delta values to be resynchronized at tempo changes within the loop – and yes, 3 of ZUN's 19 arranged MIDI files actually \u003ci\u003edo\u003c/i\u003e use more than one track, so this wasn't just about maximizing MIDI compatibility for mods. I stuck to the original approach mostly as a challenge and to prove that it's possible without first parsing the entire MIDI sequence into a friendlier internal representation, but I absolutely do \u003ci\u003enot\u003c/i\u003e recommend this to anyone else. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAfter hardcoding the loop points detected by mly into the binary, we only need to call \u003ccode\u003eMid_Proc()\u003c/code\u003e once per frame in the Music Room and pass the frame delta time instead of the 10\u0026nbsp;ms constant. And then, we get this:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.webp?aa1fa73e\" preload=\"none\" controls loop width=\"640\" height=\"480\" data-fps=\"60\" data-frame-count=\"985\" style=\"aspect-ratio: 640 / 480\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.avi?26b29bc3\"\u003e\u003csource src=\"/blog/static/video/av1/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.webm?7644d265\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.webm?339d0680\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.webm?ad0e083d\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's Music Room as seen in the P0275 build, showing off MIDI visualization despite playing back the Sound Canvas VA rendering of the OST version of the ending theme (ハーセルヴズ). \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-Music-Room-MIDI-Waveform-sync.avi?26b29bc3\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"500\" data-title=\"Loop\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe \u003ccode\u003eMIDI TIMER\u003c/code\u003e now shows off the arguably more interesting current MIDI pulse value rather than just formatting the \u003ccode\u003ePASSED TIME\u003c/code\u003e in milliseconds. Ironically, displaying this value in a constantly counting way takes \u003ci\u003emore\u003c/i\u003e effort now – the new nanosecond-based timing code doesn't use any measure of total MIDI pulses anymore, and they don't naturally fall out of the algorithm either. Instead, the code remembers the total pulse value of the last event it processed and adds the real-time duration that has passed since, similar to the original timing algorithm.\u003cbr\u003e\n\t\tThis naturally causes the timer to jump from the loop end pulse to the loop start pulse, proving that \u003ccode\u003eMid_Proc()\u003c/code\u003e is in fact looping the sequence.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"bgmpacks-2024-03-09\"\u003e\u003cp\u003e\n\tAlright, now we know what to package:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\tWe're going to have 8 \u003ci\u003eBGM packs\u003c/i\u003e for each permutation of soundtrack (OST\u0026nbsp;/ AST), sound source (Romantique Tp\u0026nbsp;/ Sound Canvas VA), and codec (FLAC\u0026nbsp;/ Vorbis), making up 1.15\u0026nbsp;GiB of music data in total.\u003cbr\u003e\n\t\tWhen looking at the package names, you will notice that I don't particularly highlight the FLAC versions as \u003cq\u003elossless\u003c/q\u003e. And for good reason – the Romantique Tp recordings had \u003ca href=\"https://www.shrinemaiden.org/forum/index.php?topic=18989.msg1218444#msg1218444\"\u003edithering and noise shaping applied to them\u003c/a\u003e, and the Sound Canvas VA versions will necessarily have to be volume-normalized and quantized to 16-bit during the conversion to FLAC. If we wanted a BGM pack with the \u003ci\u003eactual\u003c/i\u003e raw Sound Canvas VA output, we'd have to implement WavPack support, which is the only lossless codec that supports 32-bit float – and even that codec could only compress these files down to 14\u0026nbsp;MiB per minute of music, or 508\u0026nbsp;MB for the entire original soundtrack. That's 1.4× the size of an equivalent \u003ccode\u003ethbgm.dat\u003c/code\u003e! \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\tThe whole packaging process will be complex enough to warrant \u003ca href=\"https://github.com/nmlgc/BGMPacks/blob/P0269/Tupfile.lua\"\u003ea build system\u003c/a\u003e. I'd also like to generate an extensive README file for each package, not least to describe the Sound Canvas VA rendering and loop-cutting process in complete detail.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tThe AST packs need to bundle the MIDI files from ZUN's site for Music Room visualization. We might as well add a 9\u003csup\u003eth\u003c/sup\u003e MIDI-only AST pack then, as it will naturally fall out of the packaging pipeline anyway. Some people sure love their MIDI synths, after all.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tThe OST packs can fall back on the original game's MIDI files from \u003ccode\u003eMUSIC.DAT\u003c/code\u003e for their Music Room visualization, so there's no need to bundle those and infringe copyright. Ironically, the game will still require a \u003ccode\u003eMUSIC.DAT\u003c/code\u003e even \u003ci\u003eif\u003c/i\u003e you use a BGM pack, if only for the one number in that file that says that Shuusou Gyoku's soundtrack consists of 20 tracks in total.\n\t\u003c/li\u003e\u003cli\u003e\n\t\tZUN didn't arrange \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Name registration theme\"\u003eタイトルドメイド\u003c/span\u003e, so we need to copy the OST version recorded with the respective sound source into the AST pack.\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUnfortunately, we \u003ci\u003estill\u003c/i\u003e haven't reached the end of the complications and weird issues that haunt Shuusou Gyoku's music:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003e\u003cp\u003eThe original game reads the in-game track title directly out of the first Sequence Name event of the playing MIDI file. The waveform equivalent would be the Vorbis comment \u003ccode\u003eTITLE\u003c/code\u003e tag, which therefore should exactly match the original track's title, down to the exact placement of whitespace. As usual, if I emphasize minor things like this, it's not without reason: \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Stage 6 Boss/VIVIT-captured-'s second theme\"\u003e幻想科学 ～ Doll's Phantom\u003c/span\u003e inconsistently uses halfwidth spaces at both sides of the \u003cspan lang=\"ja\"\u003e～\u003c/span\u003e, and wouldn't fit into the Music Room's limited space otherwise.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eHowever, the AST MIDI files jam a bunch of other metadata into their Sequence Names, roughly following the format\n\t\u003cpre\u003e【 $title 】 from 秋霜玉  for sc88Pro comp.ZUN\u003c/pre\u003e\n\tThe track titles should definitely not appear in this format in-game, but how do we get rid of this format without hardcoding either the names or the magic to parse the names out of this format? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe absolute state of GS SysEx tooling rears its ugly head one final time in three of the AST MIDIs, which for some reason are missing the Roland vendor prefix byte in all of their SysEx messages and are therefore undeniably bugged. There even seemed to be another SysEx-related bug which \u003ca href=\"https://twitter.com/Romantique_Tp/status/1734985007616151562\"\u003eRomantique Tp explained away\u003c/a\u003e, but not this one:\u003c/p\u003e\u003cfigure id=\"sysex-bugs-2024-03-09\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cdiv data-title=\"ZUN's MIDI files\" class=\"active\"\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_04.mid\u003c/code\u003e\u003c/h4\u003e\n\t\t\t\u003cpre\u003e0:000\tSysEx(   10 42 12 40 00 7F 00 41 F7)\n0:240\tSysEx(   10 42 12 40 01 30 14 7B F7)\n0:360\tSysEx(   10 42 12 40 01 33 14 78 F7)\n0:420\tSysEx(   10 42 12 40 01 34 50 3B F7)\u003c/pre\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_05.mid\u003c/code\u003e\u003c/h4\u003e\n\t\t\t\u003cpre\u003e0:000\tSysEx(   10 42 12 40 00 7F 00 41 F7)\n0:240\tSysEx(   10 42 12 40 01 30 14 7B F7)\n0:360\tSysEx(   10 42 12 40 01 33 00 0C F7)\n0:420\tSysEx(   10 42 12 40 01 34 14 77 F7)\u003c/pre\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_10.mid\u003c/code\u003e\u003c/h4\u003e\n\t\t\t\u003cpre\u003e0:000\tSysEx(   10 42 12 40 00 7F 00 41 F7)\n0:240\tSysEx(   10 42 12 40 01 30 14 7B F7)\n0:360\tSysEx(   10 42 12 40 01 33 00 0C F7)\n0:420\tSysEx(   10 42 12 40 01 34 60 2B F7)\u003c/pre\u003e\n\t\t\u003c/div\u003e\u003cdiv data-title=\"Fixed versions\"\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_04.mid\u003c/code\u003e\u003c/h4\u003e\n\u003cpre\u003e0:000\tSysEx(41 10 42 12 40 00 7F 00 41 F7)\tGS Reset\n0:240\tSysEx(41 10 42 12 40 01 30 14 7B F7)\tReverb Macro #20\n0:360\tSysEx(41 10 42 12 40 01 33 14 78 F7)\tReverb Level 20\n0:420\tSysEx(41 10 42 12 40 01 34 50 3B F7)\tReverb Time 80\u003c/pre\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_05.mid\u003c/code\u003e\u003c/h4\u003e\n\u003cpre\u003e0:000\tSysEx(41 10 42 12 40 00 7F 00 41 F7)\tGS Reset\n0:240\tSysEx(41 10 42 12 40 01 30 14 7B F7)\tReverb Macro #20\n0:360\tSysEx(41 10 42 12 40 01 33 00 0C F7)\tReverb Level 0\n0:420\tSysEx(41 10 42 12 40 01 34 14 77 F7)\tReverb Time 20\u003c/pre\u003e\n\t\t\t\u003ch4\u003e\u003ccode\u003essg_10.mid\u003c/code\u003e\u003c/h4\u003e\n\u003cpre\u003e0:000\tSysEx(41 10 42 12 40 00 7F 00 41 F7)\tGS Reset\n0:240\tSysEx(41 10 42 12 40 01 30 14 7B F7)\tReverb Macro #20\n0:360\tSysEx(41 10 42 12 40 01 33 00 0C F7)\tReverb Level 0\n0:420\tSysEx(41 10 42 12 40 01 34 60 2B F7)\tReverb Time 96\u003c/pre\u003e\n\t\t\u003c/div\u003e\n\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tThe irony of using invalid Reverb Macros within already invalid SysEx messages is not lost on me.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\t\u003cp\u003eThis is something we should fix even before running these files through Sound Canvas VA in order to render these with the reverb settings that ZUN clearly (and, for once, unironically) intended.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eFor perfect preservation of the original BGM/gameplay synchronicity, it makes sense for the waveform versions to retain the leading 1 or 2 beats of silence that the original MIDI files use for their SysEx setup. While some of the AST tracks use a slightly different tempo compared to their OST counterparts, they would still be largely in sync as ZUN didn't rearrange the layout of their setup area… \u003ci\u003eexcept\u003c/i\u003e for, once again, the three tracks used in the Extra Stage. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Marisa's and Reimu's boss themes aren't \u003ci\u003etoo\u003c/i\u003e bad with their 4 beats of setup, but \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Extra Stage theme\"\u003eシルクロードアリス\u003c/span\u003e takes the cake with a whopping 12 beats of leading silence. That's 5 seconds from the start of the Extra Stage to the first note you'd hear. 🐌\u003c/p\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\t2) and 4) could theoretically be worked around in Shuusou Gyoku's MIDI code, but there's no way around editing the MIDI files themselves as far as 3) is concerned. Thus, it makes sense to apply all of the workarounds to the AST MIDIs as part of the BGM build process – parsing the titles out of the 【﻿brackets﻿】, inserting the Roland vendor prefix byte where necessary, and compressing the setup bars in the Extra Stage themes to match their OST counterparts. Adding any hidden magic to the MIDI code would only have needlessly increased complexity and/or annoyed some modder in the future who would then have to work around it.\u003cbr\u003e\n\tIdeally, these edits would involve taking the \u003ccode\u003emly dump\u003c/code\u003e output, performing the necessary replacements at a plaintext level, and rebuilding the result back into a MIDI file, bu~t we're unfortunately missing the latter feature. Luckily, someone else had the same idea 13 years ago and\n\t\u003ca href=\"https://github.com/markc/midicomp\"\u003ewrote a tool in C\u003c/a\u003e that does exactly what we need. Getting it to compile in 2024 only \u003ca href=\"https://github.com/markc/midicomp/pull/8\"\u003erequired fixing a typical C thing\u003c/a\u003e… why are students and boomers defending this antique of a language again? 🙄\n\u003c/p\u003e\u003cp\u003e\n\tThe single most glaring issue, however, is the drastic difference in volume between the individual tracks in both soundtracks. While Romantique Tp had to normalize each track to the maximum possible volume individually as a consequence of the recording process, the Sound Canvas VA renderings reveal just how inconsistent the volume levels of these MIDI files really are:\n\u003c/p\u003e\u003cfigure id=\"peaks-2024-03-09\"\u003e\n\t\u003crec98-child-switcher style=\"width: 100%;\"\u003e\n\t\t\u003cembed data-title=\"OST\" src=\"/blog/static/2024-03-09-SH01-Peaks-OST.svg?37778bf8\"\u003e\n\t\t\u003cembed data-title=\"AST\" src=\"/blog/static/2024-03-09-SH01-Peaks-AST.svg?ad3f802f\" class=\"active\"\u003e\n\t\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\n\t\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eThe peak amplitudes of every track in both soundtracks, as rendered by Sound Canvas VA at maximum volume. Looking at these, you might think that kaorin's 2007 recordings were purposely trying to preserve the clipping that would come out of an SC-88Pro if you don't manually adjust the volume knob for each song, but those recordings are still much louder than even these numbers.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo how do we interpret this? Is this a bug, because no one in their right mind would want their music to clip on purpose, and that in turn means that \u003ci\u003eeverything\u003c/i\u003e about these volume levels is arbitrary and unintentional? Or is this a quirk, and ZUN deliberately chose these volume levels for compositional reasons? It certainly would make sense for the name registration theme.\u003cbr\u003e\n\tOnce again, the AST version of \u003cspan lang=\"ja\" class=\"hovertext\" title=\"Extra Stage theme\"\u003eシルクロードアリス\u003c/span\u003e is the worst offender in this regard as well, but it might also provide some evidence for the quirk interpretation. The fact that almost all of its MIDI channels blast away at full volume might have been an accident that could have gone unnoticed if the volume knob of ZUN's SC-88Pro was turned rather low during the time he arranged this piece, but the excessive left-panning \u003ci\u003emust\u003c/i\u003e have been deliberate. Even Romantique Tp agrees:\n\u003c/p\u003e\u003cfigure style=\"width: 640px;\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-03-09-SH01-a16-SCVA.png?72603ac0\"\n\t\tdata-title=\"Sound Canvas VA\"\n\t\talt=\"Stereo waveform of the Sound Canvas VA rendering of Shuusou Gyoku's Extra Stage theme (シルクロードアリス), highlighting the excessive left-panning\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2024-03-09-SH01-a16-RomantiqueTp.png?e58d3107\"\n\t\tdata-title=\"Romantique Tp\"\n\t\talt=\"Stereo waveform of Romantique Tp's recording of Shuusou Gyoku's Extra Stage theme (シルクロードアリス), highlighting the excessive left-panning\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eIt might have even made compositional sense if \u003ccite\u003eSilk Road Alice\u003c/cite\u003e was supposed to be a \u003cq\u003e\"Western-style piece\"\u003c/q\u003e, \u003ca href=\"https://en.touhouwiki.net/wiki/Shuusou_Gyoku/Music#Extra_Stage_Theme\"\u003ebut it's not\u003c/a\u003e. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd that's with the volume already normalized. Because this one channel of this one track is almost twice as loud as anything else in the AST, we would consequently have to bring down the volume of every other arranged track \u003ci\u003eand the right channel of the same track\u003c/i\u003e by almost 50% if we wanted to maintain the volume differences between the individual tracks of the AST. In the process, we lose almost one entire bit of dynamic range. At this rate, you might even consider \u003cspan class=\"hovertext\" title=\"In the literal sense of the word, i.e., changing volume levels. Not rearranging.\"\u003eremixing\u003c/span\u003e and remastering the entire thing, but that would involve so many creative decisions to definitely fall into fanfiction territory…\n\u003c/p\u003e\u003cp\u003e\n\tHowever, normalizing each track to a peak level of 0\u0026nbsp;dBFS makes much more sense for in-game playback if you consider how loud Shuusou Gyoku's sound effects are. Once again, the best solution would involve offering both versions, but should we really add two more SCVA BGM packs just to cover \u003ci\u003evolume differences\u003c/i\u003e? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\t\u003ca href=\"https://en.wikipedia.org/wiki/ReplayGain\"\u003eReplayGain\u003c/a\u003e solves this exact problem for regular music listening in a non-destructive way by writing the per-track and per-album gain levels into an audio file's metadata. Since we need metadata support for titles anyway, we can do something similar, albeit not \u003ci\u003eexactly\u003c/i\u003e the same for two reasons:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eReplayGain is specified to target an \u003ca href=\"http://stephan.win31.de/music.htm#rg-levels\"\u003e\u003ci\u003eaverage\u003c/i\u003e volume of −17\u0026nbsp;dBFS\u003c/a\u003e, whereas we'd like to target a \u003ci\u003epeak\u003c/i\u003e volume of 0\u0026nbsp;dBFS in order to always use the entire available digital scale. We've got some loud sound effects to compete with, after all.\u003c/li\u003e\n\t\u003cli\u003eReplayGain expresses its gain values in dB, which is cumbersome to work with. In the realm of PCM, volume changes don't need to involve more than a simple multiplication, so let's go with a simple scalar \u003ccode\u003eGAIN FACTOR\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd so, we hard-apply the \u003ci\u003ealbum-level\u003c/i\u003e gain during the conversion from 32-bit float to FLAC to preserve the volume differences between the tracks, calculate the \u003ci\u003etrack-level\u003c/i\u003e \u003ccode\u003eGAIN FACTOR\u003c/code\u003e based on the resulting peak levels, add a volume normalization toggle to the \u003ccode\u003eSound / Config\u003c/code\u003e menu, enable it by default, and thus make everyone happy. ✅\n\u003c/p\u003e\u003cp\u003e\n\tThe final interesting tidbit in building these packages can be found in the way the Sound Canvas VA recordings are looped. When manually cutting loops, you always have to consider that the intro might end with unique notes that aren't present at the end of the loop, which will still be fading out at the calculated loop start point. This necessitates shifting the loop start point by a few bars until these notes are no longer audible – or you could simply ignore the issue because ZUN's compositions are so frantic that no one would ever notice. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tWith the separate intro and loop files generated by mly, on the other hand, the reverb/release trails are immediately visible and, after trimming trailing silence, exactly define the number of samples that the calculated loop start point needs to be shifted by. The \u003ccode\u003e.loop\u003c/code\u003e file then remains always exactly as long, \u003ci\u003ein samples\u003c/i\u003e, as the duration of the loop reported by mly. If a piece happens to have a constant tempo whose beat duration corresponds to an integer number of samples, we get some very satisfying, round loop durations out of this process. ☺️\n\u003c/p\u003e\u003chr id=\"libs-2024-03-09\"\u003e\u003cp\u003e\n\tSo let's play it all back in-game… and immediately run into two unexpected miniaudio limitations, what the…?!\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eminiaudio uses a fixed linear function for its fade-out envelope, and doesn't offer anything else? We might not even want a \u003ca href=\"https://www.dr-lex.be/info-stuff/volumecontrols.html\"\u003elogarithmic one\u003c/a\u003e this time because \u003ca href=\"https://music.stackexchange.com/questions/123032/why-is-midi-gain-based-on-a-factor-of-40/123067#123067\"\u003esymmetry with MIDI's simple quadratic curve would be neat\u003c/a\u003e, but we sure don't want a linear function – those stay near the original volume for too long, and then turn quiet way too quickly.\u003c/li\u003e\n\t\u003cli\u003eThere is no way to access FLAC metadata from miniaudio's public API, even though the library bundles the author's own FLAC library which has this feature?\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2023-09-30#miniaudio-2023-09-30\"\u003e📝 Back when I evaluated miniaudio\u003c/a\u003e, I alluded that I consider single-file C libraries to be massively overrated, and this is exactly why: Once they grow as massive as miniaudio (how ironic), they can quickly lead to their authors treating their dependencies as implementation details and melting down the interfaces that would naturally arise. In a regular library, dr_flac would be a separate, proper dependency, and the API would have a way to initialize a stream from an externally loaded \u003ccode\u003edrflac\u003c/code\u003e object. But since the C community collectively pretends that multi-file libraries are a burden on other developers, miniaudio ended up with dr_flac copy-pasted into its giant single file, with a silly \u003ccode\u003ema_\u003c/code\u003e namespacing prefix added to all its functions. And why? Did we have to move so far in the other direction just because \u003ca href=\"https://stackoverflow.com/a/18538444\"\u003eCMake doesn't support globbing\u003c/a\u003e? That's a symptom of CMake not actually solving any problem, not a valid architectural decision that libraries should bend around. 🙄\u003cbr\u003e\n\tSo unless we fork and hack around in miniaudio, there's now no way around depending on a second, regular copy of dr_flac. Which has now led to the same project organization bloat that single-file libraries originally set out to prevent…\n\u003c/p\u003e\u003cp\u003e\n\tSigh. At this rate, it makes more sense to just copy-paste and adapt the old BGM streaming code I wrote for thcrap in late 2018, which used dr_flac directly, and extend it with metadata support. With the streaming code moved out of the platform layer and into game logic, it also makes much more sense to implement the squared fade-out curve at that same level instead of copy-pasting and adjusting an unhealthy amount of miniaudio's verbose C code.\u003cbr\u003e\n\tWhile I'm doing the same for the old Vorbis streaming code, it would also make sense to rewrite that one to use stb_vorbis instead of the old libogg+libvorbis reference libraries. There's no need to add two more dependencies if miniaudio already comes with \u003ccode\u003estb_vorbis.c\u003c/code\u003e, and that library is \u003ca href=\"https://phoboslab.org/log/2023/02/qoa-time-domain-audio-compression\"\u003ewidely\u003c/a\u003e \u003ca href=\"https://twitter.com/flibitijibibo/status/1717599317333074213\"\u003eacclaimed\u003c/a\u003e. So, integration should be a breeze, right?\u003cbr\u003e\n\tWell, surprise, rarely have I seen a C library so actively hostile toward being integrated. Both of its API variants are completely unreasonable:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe \u003cdfn\u003epulldata\u003c/dfn\u003e API pulls Vorbis data as needed from either a memory buffer containing the entire Vorbis file, or a C \u003ccode\u003eFILE*\u003c/code\u003e handle.\u003cbr\u003e\n\tEffectively, this forces either you to give up disk streaming completely, or your program into C's terrible I/O API with all its buffering slowness and Unicode issues on Windows. The documentation even goes on to suggest \u003ca href=\"https://github.com/nothings/stb/blob/ae721c50eaf761660b4f90cc590453cdb0c2acd0/stb_vorbis.c#L250-L255\"\u003ejust modifying the code if you need anything else\u003c/a\u003e, which might be acceptable in the strange world of game development this library originates from, but it sure isn't in the kind of open-source development I do.\n\t\u003c/li\u003e\n\t\u003cli\u003eThe \u003cdfn\u003epushdata\u003c/dfn\u003e API expects the caller to gradually feed chunks of Vorbis data. How large do these chunks have to be? Nobody knows – and, even worse, the API doesn't retain \u003ci\u003eany\u003c/i\u003e of the data already pushed in. If the buffer you passed is too small, which you don't get to know in advance, you have to pass the same data \u003ci\u003eplus\u003c/i\u003e more in the next call. I get that you might want an API like this to avoid dynamic memory allocations, but not only does this API perform plenty of allocations itself, it actively forces its caller to \u003ccode\u003erealloc()\u003c/code\u003e over and over again. 🙄 The lack of seeking support reveals that this API is geared towards live-streamed audio, and it might very well be acceptable in such a case, but it's nothing we could use for BGM.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWhat happened to the tried-and-true idea of providing a structure with read, tell, and seek callbacks, and then providing an optional variant for C \u003ccode\u003eFILE*\u003c/code\u003e handles if you absolutely must? Sure, the whole point of Vorbis is to be small and nobody these days would care about spending a few MB on keeping an entire Vorbis file in memory, but \u003ci\u003ecome on\u003c/i\u003e. If \u003ci\u003epulldata\u003c/i\u003e made the deliberate and opinionated choice to only support buffers of complete Vorbis streams and argued in the name of simplicity that \u003cspan class=\"hovertext\" title=\"And I'm not just assuming the typical background tracks for games that aren't larger than a few MB. With memory-mapped I/O and the 48-bit address-space for 64-bit programs, this can work just as well for giant files.\"\u003ehand-coded disk streaming isn't worth it in this day and age\u003c/span\u003e, I might have even been convinced. And this is from the guy who popularized the concept of single-file C libraries in the first place? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tOh well, \u003ca href=\"https://github.com/nmlgc/ssg/blob/P0275/Tupfile.lua#L96-L116\"\u003etupblocks go brrr\u003c/a\u003e. libvorbis definitely shows its age with all the old command-line tools in the \u003ccode\u003elib/\u003c/code\u003e directory that they never moved away and that we now have to remove from our glob. But even that just adds \u003ca href=\"https://github.com/nmlgc/ssg/blob/P0275/Tupfile.lua#L112\"\u003ea single line to the Tupfile\u003c/a\u003e, and then we get to enjoy its much friendlier API. That sure beats the almost 800 lines of code that miniaudio had to write to integrate stb_vorbis… which I can't even link because the file is too big for GitHub. 🤷\u003cbr\u003e\n\tAt this point, it would have even made sense to upgrade from a 24-year-old lossy codec to an 11-year-old lossy codec and use Opus instead, since the enforced 48,000\u0026nbsp;Hz sampling rate is a non-issue when you control the entire audio pipeline. But let's keep compatibility with existing thcrap mods for now.\n\u003c/p\u003e\u003cp\u003e\n\tThe last time I added dependencies, \u003ca href=\"/blog/2023-09-30#sdl-2023-09-30\"\u003e📝 I wondered whether just downloading and extracting official Windows binary builds might be superior to pasting batch script duct tape over the usability issues of Git submodules\u003c/a\u003e. However, I still wanted to try out Git's \u003ca href=\"https://git-scm.com/docs/git-sparse-checkout/2.44.0\"\u003esparse checkout\u003c/a\u003e feature before, in an attempt to remove all the unneeded bloat… and as it turned out, this might just be the idealistic and perfect nirvana of vendoring libraries in C++ projects. I particularly like how the \u003ca href=\"https://git-scm.com/docs/git-sparse-checkout/2.44.0#_internalscone_mode_handling\"\u003elimitations of its default mode\u003c/a\u003e (always checking out all files within each directory level that shows up in a filter) can be turned into a guideline about how to structure a repository: All non-essential stuff that consumers of your code might not need – tests, high-level documentation, or optional features – should go into a subdirectory where it can be easily filtered.\u003cbr\u003e\n\tAnd that's how the size of our \u003ccode\u003elibs/\u003c/code\u003e directory went down from 82.7\u0026nbsp;MiB in the P0256 build to 30.4\u0026nbsp;MiB in the P0275 build, despite \u003ci\u003eadding\u003c/i\u003e 4 more libraries in the latter. Now if only this didn't require \u003ca href=\"https://github.com/nmlgc/ssg/blob/P0275/build.bat#L40-L55\"\u003eeven more duct tape to actually set up shallow clones correctly\u003c/a\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, the Windows build ended up using only a single one of the miniaudio features that DirectSound doesn't have, and that's the ability to use the more modern WASAPI \u003ci\u003einstead\u003c/i\u003e of DirectSound. We're still going to use miniaudio for the Linux port, but as far as Windows is concerned, it would be quite nice to backport BGM streaming to the game's original DirectSound backend. The P0275 build is pushing 1\u0026nbsp;MiB of binary size for a game that originally came in a 220\u0026nbsp;KiB binary, so it would remove a noticeable amount of bloat from \u003ccode\u003eGIAN07.EXE\u003c/code\u003e, but it would also allow waveform BGM to work in the Windows 98-compatible i586 build. If that sounds cool to you, \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/53\"\u003ethis is the issue you want to fund\u003c/a\u003e.\n\u003c/p\u003e\u003chr id=\"impl-2024-03-09\"\u003e\u003cp\u003e\n\tThat only left some logic and UI busywork to put it all together, which means that we've almost reached the end of things to talk about! Here's what it all looks like:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eBGM pack selection is done in-game through a new submenu. The \u003ccode\u003e\u0026lt;Download\u0026gt;\u003c/code\u003e option will open the BGM pack release page in the system's preferred browser:\n\t\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\t\u003cimg\n\t\t\t\tsrc=\"/blog/static/2024-03-09-SH01-BGM-pack-menu.png?d5d1a5bf\"\n\t\t\t\talt=\"Screenshot of the BGM pack selection menu introduced in the Shuusou Gyoku P0275 build, highlighting the \u0026lt;Download\u0026gt; option.\"\n\t\t\t\u003e\n\t\t\t\u003cfigcaption\u003e\n\t\t\t\tThis window presented a great occasion for already implementing the generic boilerplate for vertically scrolling windows with an unlimited number of items. That will come in quite handy once we \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/36\"\u003eintroduce better replay support\u003c/a\u003e… 👀\n\t\t\t\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\t\u003cli\u003eEven with per-track BGM volume normalization, Shuusou Gyoku's sound effects are still a bit too loud in comparison, especially when mixed on top of that excessively and unfixably left-panned AST version of the Extra Stage theme. Adding separate volume controls for BGM and sound effects really was the only sustainable solution here, and conveniently checks an important quality-of-life box the original game lacked. So important that it was \u003ca href=\"https://github.com/nmlgc/ssg/issues/1\"\u003ethe very first issue I added to the GitHub tracker of my fork\u003c/a\u003e:\n\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-SH01-Sound-Music-options.png?6c2b9765\"\n\t\t\talt=\"Screenshot of the Sound / Music menu of the Shuusou Gyoku P0275 build, showing off the new volume control and BGM pack selection options.\"\n\t\t\t\u003e\n\t\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eI really wanted to have Japanese help text in these menus, as it makes them look just so much more consistent and polished. Many thanks to \u003ca href=\"https://twitter.com/Wafflesespeon24\"\u003eElfin\u003c/a\u003e, who responded to my bounty offer, and will most likely also provide localizations for future features.\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eIn-game music titles are now consistently right-aligned. Leading whitespace in 4 of the original MIDI Sequence Names suggests that pbg might have intended these titles to be centered within the 216 maximum pixels that the original code designated for music titles, but none of those 4 had the correct amount of spaces that would have been required for exact centering:\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003e#04 |・・・幻想帝都・・・・\n#07 |・・天空アーミー・・・\n#11 |・魔法少女十字軍・・・\n#16 |・・シルクロードアリス\u003c/pre\u003e\u003c/figure\u003e\n\tRight-aligned text matches the one certain intention I can read out of the code, and allows us to consistently trim whitespace from both the original MIDI Sequence Names and the \u003ccode\u003eTITLE\u003c/code\u003e tags in the BGM packs… at the cost of significantly changing the animation. 🤔 \u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-Ingame-music-title-original.webp?ede6def2\" preload=\"none\" controls data-title=\"Original game\" loop data-active width=\"384\" height=\"128\" data-fps=\"60\" data-frame-count=\"492\" style=\"aspect-ratio: 384 / 128\" data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-Ingame-music-title-original.avi?d93b3995\"\u003e\u003csource src=\"/blog/static/video/av1/2024-03-09-SH01-Ingame-music-title-original.webm?b16c5ca1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-Ingame-music-title-original.webm?58927ea2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-Ingame-music-title-original.webm?63167681\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's original in-game music title animation for Stage 2, demonstrating an instance where a track title that appears almost centered. \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-Ingame-music-title-original.avi?d93b3995\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-03-09-SH01-Ingame-music-title-P0275.webp?ede6def2\" preload=\"none\" controls data-title=\"P0275 build\" loop width=\"384\" height=\"128\" data-fps=\"60\" data-frame-count=\"492\" style=\"aspect-ratio: 384 / 128\" data-lossless=\"/blog/static/video/zmbv/2024-03-09-SH01-Ingame-music-title-P0275.avi?80d3ba7e\"\u003e\u003csource src=\"/blog/static/video/av1/2024-03-09-SH01-Ingame-music-title-P0275.webm?092f2938\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-03-09-SH01-Ingame-music-title-P0275.webm?8ffa206b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-03-09-SH01-Ingame-music-title-P0275.webm?427e70e9\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's in-game music title animation for Stage 2 as seen in the P0275 build, using correct right-alignment which unfortunately affects the animation. \u003ca href=\"/blog/static/video/zmbv/2024-03-09-SH01-Ingame-music-title-P0275.avi?80d3ba7e\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tMaybe, all this whitespace had the explicit purpose of making the animation look the way it did originally? But hard-padding the title tags in the BGM packs would be \u003ci\u003eso dumb…\u003c/i\u003e 😩 Let's keep it like this for now and \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/55\"\u003efix the animation later\u003c/a\u003e.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eAt startup, the game now shows a new screen if any of the game's .DAT files are missing, displaying their expected absolute path. This is bound to be very important on Linux because each distribution might have its own idea of where these files are supposed to be stored. But even on Windows, this allows \u003ccode\u003eGIAN07.EXE\u003c/code\u003e to at least \u003ci\u003erun\u003c/i\u003e and show something if one or more of these files are not present, instead of crashing at the first attempt of loading anything from them.\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-03-09-SH01-Missing.png?4484dece\"\n\t\t\talt=\"The new 'missing game data' screen shown at startup in the P0275 build of Shuusou Gyoku, listing any missing files at their absolute directories and offering the possibility to recheck their existence without quitting the game.\"\n\t\t\u003e\n\t\t\u003cfigcaption\u003eThe \u003ccode\u003e¥\u003c/code\u003e instead of \u003ccode\u003e\\\u003c/code\u003e is, \u003ca href=\"/blog/2022-11-30#ref-2022-11-30\"\u003e📝 once again\u003c/a\u003e, a font issue. Good luck finding a font not named MS Gothic that looks good when rendered in this game…\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eOn a more unfortunate note, I dropped the i586 build from this release. Visual Studio 2022's CRT implements the new filesystem and threading code using Win32 API functions that are only available on Vista or later and are not covered by the one ready-made KernelEx package I was able to find, so I couldn't easily test such a build on Windows 98 anymore. Resurrecting the i586 build would therefore involve additional platform abstraction layers that we wouldn't need otherwise. \u003ca href=\"https://github.com/nmlgc/ssg/issues/58\" class=\"goal\"\u003eWriting them wouldn't be too expensive\u003c/a\u003e, but it only makes sense if there's actual demand. \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/53\"\u003eBackporting waveform BGM to DirectSound\u003c/a\u003e to restore feature parity would also be a good idea here, as it would avoid the need to litter the current code with \u003ccode\u003e#ifdef\u003c/code\u003es at any place that references anything related to BGM packs.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0275\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0275\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/BGMPacks/releases/tag/P0269\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku SC-88Pro BGM packs\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca class=\"release\" href=\"https://github.com/nmlgc/mly/releases/tag/v0.1.0\"\u003e\n\tmly 0.1.0\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"pricing-2024-03-09\"\u003e\u003cp\u003e\n\tAfter half a year of being bought out way past the cap, I've finally got some small room left for new orders again. If it weren't for this blog post and the required research and web development work, this delivery would have probably come out in early January, taking half the time it ended up taking. So I really have to start factoring the blog posts into the push prices in a better and fairer way.\u003cbr\u003e\n\tMeanwhile, the hate toward my day job only keeps growing, but there's little point in looking for a new one as long as ReC98 remains this motivating and complex. It leaves pretty much no cognitive room for any similarly demanding job. Thus, I want 2024 to be the year where ReC98 either becomes profitable enough to be my only full-time job, or where we conclusively find out that it can't, I go look for a better day job, and ReC98 shifts to a slower pace. Here's the plan:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFrom now on, I will immediately increase the push price whenever we reach 100% of the cap, either directly through new orders or indirectly through existing subscriptions. The price increase will be relative to how long it took to reach that point since the last re-opening.\u003c/li\u003e\n\t\u003cli\u003eIf the store continues selling out, I will aim for \u003cscript\u003eformatCurrency(25000)\u003c/script\u003e\u003cnoscript\u003e250.00\u0026nbsp;€\u003c/noscript\u003e per push by the end of the year.\u003c/li\u003e\n\t\u003cli\u003eIn exchange, microtransactions (i.e., deliveries containing just code and no blog posts) will now be half the price of regular pushes for the same amount of delivered code. Or in other words: If you want to fund a goal that's eligible for microtransactions, you can now decide whether your fixed amount of money goes to 2× coding work and 0× blogging, or 1× coding work and 1× blogging.\u003c/li\u003e\n\t\u003cli\u003e I'll permanently increase the default level of the cap from 8 to 10 pushes. The past 12 months were full of mod releases that raised the bar, and 2024 shows no signs of stopping that trend.\u003c/li\u003e\n\t\u003cli\u003eIf we ever reach \u003cscript\u003eformatCurrency(40000)\u003c/script\u003e\u003cnoscript\u003e400.00\u0026nbsp;€\u003c/noscript\u003e per push, I plan to hire people for some of the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/contribution-ideas\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/a\u003e\u003c/span\u003e or anything else that might improve this project. (Well-produced YouTube videos about the findings of this project might be a nice idea!) At that point, I will have reached my goal of living decently off this project alone, and it's time for others to make money in this space as well.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWith the new price of \u003cscript\u003eformatCurrency(12500)\u003c/script\u003e\u003cnoscript\u003e125.00\u0026nbsp;€\u003c/noscript\u003e per push, this means that there's now a small window in which you can get a full push worth of functionality for \u003cscript\u003eformatCurrency(6250)\u003c/script\u003e\u003cnoscript\u003e62.50\u0026nbsp;€\u003c/noscript\u003e, until the current cap is filled up again.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Probably TH02's endings to relax a bit. Maybe we're also getting some new Touhou-related contributions?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-04-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2024-02-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-03-09T15:23:29Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2024-02-03",
      "url": "https://rec98.nmlgc.net/blog/2024-02-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-03-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2024-02-03\"\u003e\u003ctime datetime=\"2024-02-03T08:03:19Z\"\u003e2024-02-03 08:03\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0264\"\u003eP0264\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (Music Rooms, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/46cd6e7...78728f6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0265\"\u003eP0265\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (Music Rooms, part 2/2 + MAINE.EXE main()) + TH02 PI/RE (Boss damage and position)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/78728f6...ff19bed\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous], iruleatgames\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/policy-bugfix\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/stones\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH02\u0026#39;s Stage 3 boss. Arguably the best Touhou character.\"\u003estones\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/good-code\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cp\u003e\n\tOh, it's 2024 already and I didn't even have a delivery for December or January? Yeah… I can only repeat what I said at the end of November, although the finish line is actually in sight now. With 10 pushes across 4 repositories and a blog post that has already reached a word count of 9,240, the Shuusou Gyoku SC-88Pro BGM release is going to break \u003ca href=\"/blog/2022-01-31\"\u003e📝 both the push record set by TH01 Sariel two years ago\u003c/a\u003e, and \u003ca href=\"/blog/2023-09-30\"\u003e📝 the blog post length record set by the last Shuusou Gyoku delivery\u003c/a\u003e. Until that's done though, let's clear some more PC-98 Touhou pushes out of the backlog, and continue the preparation work for the non-ASCII translation project starting later this year.\n\u003c/p\u003e\u003cp\u003e\n\tBut first, we got another \u003ca href=\"/faq#mod-bugs\"\u003efree bugfix according to my policy\u003c/a\u003e! \u003ca href=\"/blog/2022-04-18#marisa-2022-04-18\"\u003e📝 Back in April 2022 when I researched the \u003ccode\u003eDivide Error\u003c/code\u003e crash that can occur in TH04's Stage 4 Marisa fight\u003c/a\u003e, I proposed and implemented four possible workarounds and let the community pick one of them for the generally recommended small bugfix mod. I still pushed the others onto individual branches in case the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e community ever wants to look more closely into them and maybe pick a different one… except that I accidentally pushed the wrong code for the warp workaround, probably because I got confused with the second warp variant I developed later on.\u003cbr\u003e\n\tFortunately, I still had the intended code for both variants lying around, and used the occasion to merge the current \u003ccode\u003emaster\u003c/code\u003e branch into all of these mod branches. Thanks to wyatt8740 for \u003ca href=\"https://github.com/nmlgc/ReC98/issues/11\"\u003espotting and reporting this oversight\u003c/a\u003e!\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#b-2024-02-03\"\u003eThe Music Room background masking effect\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#grcg-2024-02-03\"\u003eThe GRCG's plane disabling flags\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#colors-2024-02-03\"\u003eText color restrictions\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#mess-2024-02-03\"\u003eThe entire messy rest of the Music Room code\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#cong-2024-02-03\"\u003eTH04's partially consistent congratulation picture on Easy Mode\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-pi-2024-02-03\"\u003eTH02's boss position and damage variables\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"b-2024-02-03\"\u003e\u003cp\u003e\n\tAs the final piece of code shared in largely identical form between 4 of the 5 games, the Music Rooms were the biggest remaining piece of low-hanging fruit that guaranteed big finalization% gains for comparatively little effort. They seemed to be especially easy because I already decompiled TH02's Music Room together with the rest of that game's \u003ccode\u003eOP.EXE\u003c/code\u003e back in early 2015, when this project focused on just raw decompilation with little to no research. 9 years of increased standards later though, it turns out that I missed \u003ci\u003ea lot\u003c/i\u003e of details, and ended up renaming most variables and functions. Combined with larger-than-expected changes in later games and the usual quality level of ZUN's menu code, this ended up taking noticeably longer than the single push I expected.\n\u003c/p\u003e\u003cp\u003e\n\tThe undoubtedly most interesting part about this screen is the animation in the background, with the spinning and falling polygons cutting into a single-color background to reveal a spacey image below. However, the only background image loaded in the Music Room is \u003ccode\u003eOP3.PI\u003c/code\u003e (TH02/TH03) or \u003ccode\u003eMUSIC3.PI\u003c/code\u003e (TH04/TH05), which looks like this in a .PI viewer or when converted into another image format with the usual tools:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH02-Music-Room-background-B-file.png?b1164883\"\n\t\t\tdata-title=\"TH02\"\n\t\t\talt=\"TH02's Music Room background in its on-disk state\"\n\t\t\tclass=\"active\"\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH03-Music-Room-background-B-file.png?7486d2bb\"\n\t\t\tdata-title=\"TH03\"\n\t\t\talt=\"TH03's Music Room background in its on-disk state\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH04-Music-Room-background-B-file.png?e2b266e7\"\n\t\t\tdata-title=\"TH04\"\n\t\t\talt=\"TH04's Music Room background in its on-disk state\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH05-Music-Room-background-B-file.png?f54c7b15\"\n\t\t\tdata-title=\"TH05\"\n\t\t\talt=\"TH05's Music Room background in its on-disk state\"\n\t\t\t\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eLet's call this \"the blank image\".\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat is definitely the color that appears on top of the polygons, but where is the spacey background? If there is no other .PI file where it could come from, it has to be somewhere in that same file, right? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAnd indeed: This effect is another bitplane/color palette trick, exactly like the \u003ca href=\"/blog/2023-05-29\"\u003e📝 three falling stars in the background of TH04's Stage 5\u003c/a\u003e. If we set every bit on the first bitplane and thus change any of the resulting even hardware palette color indices to odd ones, we reveal a full second 8-color sub-image hiding in the same .PI file:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH02-Music-Room-background-B-set.png?86a6255b\"\n\t\t\tdata-title=\"TH02\"\n\t\t\talt=\"TH02's Music Room background, with all bits in the first bitplane set to reveal the spacey background image, and the full color palette at the bottom\"\n\t\t\tclass=\"active\"\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH03-Music-Room-background-B-set.png?12a40915\"\n\t\t\tdata-title=\"TH03\"\n\t\t\talt=\"TH03's Music Room background, with all bits in the first bitplane set to reveal the spacey background image, and the full color palette at the bottom\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH04-Music-Room-background-B-set.png?040c01e3\"\n\t\t\tdata-title=\"TH04\"\n\t\t\talt=\"TH04's Music Room background, with all bits in the first bitplane set to reveal the spacey background image, and the full color palette at the bottom\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH05-Music-Room-background-B-set.png?4db5f7bc\"\n\t\t\tdata-title=\"TH05\"\n\t\t\talt=\"TH05's Music Room background, with all bits in the first bitplane set to reveal the spacey background image, and the full color palette at the bottom\"\n\t\t\t\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eThe spacey sub-image. Never before seen!1!! …OK, \u003ca href=\"https://touhou-memories.com/post/738606806243409920\"\u003etouhou-memories beat me by a month\u003c/a\u003e. Let's add each image's full 16-color palette to deliver some additional value.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tOn a high level, the first bitplane therefore acts as a stencil buffer that selects between the blank and spacey sub-image for every pixel. The important part here, however, is that the first bitplane of the blank sub-images does \u003ci\u003enot\u003c/i\u003e consist entirely of 0 bits, but does have 1 bits at the pixels that represent the caption that's supposed to be overlaid on top of the animation. Since there now are some pixels that should always be taken from the spacey sub-image regardless of whether they're covered by a polygon, the game can no longer just clear the first bitplane at the start of every frame. Instead, it has to keep a separate copy of the first bitplane's original state (called \u003ccode\u003enopoly_B\u003c/code\u003e in the code), captured right after it \u003cspan class=\"hovertext\" title=\"master.lib decodes .PI images into a packed-pixel format, and such a capture is the simplest way of retrieving an individual plane.\"\u003eblitted the .PI image to VRAM\u003c/span\u003e. Turns out that this copy also comes in quite handy with the text, but more on that later.\n\u003c/p\u003e\u003chr id=\"grcg-2024-02-03\"\u003e\u003cp\u003e\n\tThen, the game simply draws polygons onto only the reblitted first bitplane to conditionally set the respective bits. ZUN used master.lib's \u003ccode\u003egrcg_polygon_c()\u003c/code\u003e function for this, which means that we can entirely thank the uncredited master.lib developers for this iconic animation – if they hadn't included such a function, the Music Rooms would most certainly look completely different.\u003cbr\u003e\n\tThis is where we get to complete the series on the PC-98 GRCG chip with the last remaining four bits of its mode register. So far, we only needed the highest bit (\u003ccode\u003e0x80\u003c/code\u003e) to either activate or deactivate it, and the bit below (\u003ccode\u003e0x40\u003c/code\u003e) to choose between the \u003ca href=\"/blog/2020-12-18\"\u003e📝 RMW\u003c/a\u003e and \u003ca href=\"/blog/2022-01-31\"\u003e📝 TCR\u003c/a\u003e/\u003ca href=\"/blog/2023-05-29\"\u003e📝 TDW\u003c/a\u003e modes. But you can also use the lowest four bits to restrict the GRCG's operations to any subset of the four bitplanes, leaving the other ones untouched:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e// Enable the GRCG (0x80) in regular RMW mode (0x40). All bitplanes are\n// enabled and written according to the contents of the tile register.\noutportb(0x7C, 0xC0);\n\n// The same, but limiting writes to the first bitplane by disabling the\n// second (0x02), third (0x04), and fourth (0x08) one, as done in the\n// PC-98 Touhou Music Rooms.\noutportb(0x7C, 0xCE);\n\n// Regular GRCG blitting code to any VRAM segment…\npokeb(0xA8000, offset, …);\n\n// We're done, turn off the GRCG.\noutportb(0x7C, 0x00);\n\u003c/pre\u003e\u003c/figure\u003e\u003cp\u003e\n\tThis could be used for some unusual effects when writing to two or three of the four planes, but it seems rather pointless for this specific case at first. If we only want to write to a single plane, why not just do so directly, without the GRCG? Using that chip only involves more hardware and is therefore slower by definition, and the blitting code would be the same, right?\u003cbr\u003e\n\tThis is another one of these questions that would be interesting to benchmark one day, but in this case, the reason is purely practical: All of master.lib's polygon drawing functions expect the GRCG to be running in RMW mode. They write their pixels as bitmasks where 1 and 0 represent pixels that should or should not change, and leave it to the GRCG to combine these masks with its tile register and \u003ccode\u003eOR\u003c/code\u003e the result into the bitplanes instead of doing so themselves. Since GRCG writes are done via \u003ccode\u003eMOV\u003c/code\u003e instructions, not using the GRCG would turn these bitmasks into actual dot patterns, overwriting any previous contents of each VRAM byte that gets modified.\u003cbr\u003e\n\tTechnically, you'd only have to replace a few \u003ccode\u003eMOV\u003c/code\u003e instructions with \u003ccode\u003eOR\u003c/code\u003e to build a non-GRCG version of such a function, but why would you do that if you haven't measured polygon drawing to be an actual bottleneck.\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-Music-Room-polygons-without-GRCG.png?d6000369\"\n\t\t\tdata-title=\"GRCG disabled\"\n\t\t\talt=\"Three overlapping Music Room polygons rendered using master.lib's grcg_polygon_c() function with a disabled GRCG\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-Music-Room-polygons-with-GRCG.png?6b5ce789\"\n\t\t\tdata-title=\"GRCG enabled\"\n\t\t\talt=\"Three overlapping Music Room polygons rendered as in the original game, with the GRCG enabled\"\n\t\t\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tAn example with three polygons drawn from top to bottom. Without the GRCG, edges of later polygons overwrite any previously drawn pixels within the same VRAM byte. Note how treating bitmasks as dot patterns corrupts even those areas where the background image had nonzero bits in its first bitplane.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"colors-2024-02-03\"\u003e\u003cp\u003e\n\tAs far as complexity is concerned though, the worst part is the implicit logic that allows all this text to show up on top of the polygons in the first place. If every single piece of text is only rendered a single time, how can it appear on top of the polygons if those are drawn every frame?\u003cbr\u003e\n\tDepending on the game (because \u003ci\u003eof course\u003c/i\u003e it's game-specific), the answer involves either the individual bits of the text color index or the actual contents of the palette:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eColors 0 or 1 can't be used, because those don't include any of the bits that can stay constant between frames.\u003c/li\u003e\n\t\u003cli\u003eIf the lowest bit of a palette color index has no effect on the displayed color, text drawn in either of the two colors won't be visually affected by the polygon animation and will always appear on top. TH04 and TH05 rely on this property with their colors 2/3, 4/5, and 6/7 being identical, but this would work in TH02 and TH03 as well.\u003c/li\u003e\n\t\u003cli\u003eBut this doesn't apply to TH02 and TH03's palettes, so how do they do it? The secret: They simply include all text pixels in \u003ccode\u003enopoly_B\u003c/code\u003e. This allows text to use any color with an odd palette index – the lowest bit then won't be affected by the polygons \u003ccode\u003eOR\u003c/code\u003eed into the first bitplane, and the other bitplanes remain unchanged.\u003c/li\u003e\n\t\u003cli\u003eTH04 is a curious case. Ostensibly, it seems to remove support for odd text colors, probably because the new 10-frame fade-in animation on the comment text would require at least the comment area in VRAM to be captured into \u003ccode\u003enopoly_B\u003c/code\u003e on every one of the 10 frames. However, the initial pixels of the tracklist are still included in \u003ccode\u003enopoly_B\u003c/code\u003e, which would allow \u003ci\u003ethose\u003c/i\u003e to still use any odd color in this game. ZUN only removed those from \u003ccode\u003enopoly_B\u003c/code\u003e in TH05, where it \u003ci\u003ehad\u003c/i\u003e to be changed because that game lets you scroll and browse through multiple tracklists.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH02-Music-Room-nopoly-B.png?4ad1c0b9\"\n\t\t\tdata-title=\"TH02\"\n\t\t\talt=\"\"\n\t\t\tclass=\"active\"\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH03-Music-Room-nopoly-B.png?18333999\"\n\t\t\tdata-title=\"TH03\"\n\t\t\talt=\"\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH04-Music-Room-nopoly-B.png?93fa43e6\"\n\t\t\tdata-title=\"TH04\"\n\t\t\talt=\"\"\n\t\t\t\u003e\n\t\u003cimg\n\t\t\tsrc=\"/blog/static/2024-02-03-TH05-Music-Room-nopoly-B.png?139a8c24\"\n\t\t\tdata-title=\"TH05\"\n\t\t\talt=\"\"\n\t\t\t\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eThe contents of \u003ccode\u003enopoly_B\u003c/code\u003e with each game's first track selected.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr id=\"mess-2024-02-03\"\u003e\u003cp\u003e\n\tFinally, here's a list of all the smaller details that turn the Music Rooms into such a mess:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cp\u003eDue to the polygon animation, the Music Room is one of the few double-buffered menus in PC-98 Touhou, rendering to both VRAM pages on alternate frames instead of using the other page to store a background image. Unfortunately though, this doesn't actually translate to tearing-free rendering because ZUN's initial implementation for TH02 mixed up the order of the required operations. You're supposed to \u003ci\u003efirst\u003c/i\u003e wait for the GDC's VSync interrupt and \u003ci\u003ethen\u003c/i\u003e, within the display's \u003ca href=\"https://en.wikipedia.org/wiki/Vertical_blanking_interval\"\u003evertical blanking interval\u003c/a\u003e, write to the relevant I/O ports to flip the accessed and shown pages. Doing it the other way around and flipping as soon as you're finished with the last draw call of a frame means that you'll very likely hit a point where the (real or emulated) electron beam is still traveling across the screen. This ensures that there will be a tearing line \u003ci\u003esomewhere\u003c/i\u003e on the screen on all but the fastest PC-98 models that can render an entire frame of the Music Room completely within the vertical blanking interval, causing the very issue that double-buffering was supposed to prevent. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tZUN only fixed this \u003cspan class=\"hovertext\" title=\"Remember: It's only a bug if it also occurs on an infinitely fast PC-98.\"\u003elandmine\u003c/span\u003e in TH05.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2025-09-06):\u003c/strong\u003e The \u003ca href=\"/blog/2025-09-06#fp-2025-09-06\"\u003e📝 2025-09-06 blog post\u003c/a\u003e contains a visualization of this tearing landmine.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe polygons have a fixed vertex count and radius depending on their index, everything else is randomized. They are also never reinitialized while \u003ccode\u003eOP.EXE\u003c/code\u003e is running – if you leave the Music Room and reenter it, they will continue animating from the same position.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eExcept for TH05's repeatable ⬆️ up and ⬇️ down inputs, the games force you to release any pressed key before they will handle any new input. But since the game is running on a PC-98, we \u003ca href=\"/blog/2023-11-01#ref-2023-11-01\"\u003e📝 once again\u003c/a\u003e have to mention the \u003ca href=\"https://github.com/nmlgc/ReC98/commit/8dfc2cd\"\u003einfamous quirk of the keyboard controller with regard to held keys\u003c/a\u003e. Funnily enough, the games go back and forth in how they address it, in a way that matches the \u003ca href=\"/blog/2022-11-30#games-2022-11-30\"\u003e📝 additional delay of the cutscene interpreter loop\u003c/a\u003e:\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eTH02 and TH04 don't handle it at all, causing held keys to be processed again after about a second.\u003c/li\u003e\n\t\t\u003cli\u003eTH03 and TH05 correctly work around the quirk, at the usual cost of a 614.4\u0026nbsp;µs delay per frame. Except that the delay is actually twice as long in frames in which a previously held key is released, because this code is a mess. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003c/ul\u003e\n\t\u003cp\u003eBut even in 2024, DOSBox-X is the only emulator that actually replicates this detail of real hardware. On anything else, keyboard input will behave as ZUN intended it to. At least I've now mentioned this once for every game, and can just link back to this blog post for the other menus we still have to go through, in case their game-specific behavior matches this one.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eTH02 is the only game that\u003c/p\u003e\u003col\u003e\n\t\t\u003cli\u003eseparately lists the stage and boss themes of the main game, rather than following the in-game order of appearance,\u003c/li\u003e\n\t\t\u003cli\u003econtinues playing the selected track when leaving the Music Room,\n\t\t\u003c/li\u003e\n\t\t\u003cli\u003ealways loads both MIDI and PMD versions, regardless of the currently selected mode, and\u003c/li\u003e\n\t\t\u003cli\u003edoes not stop the currently playing track before loading the new one into the PMD and MMD drivers.\u003c/li\u003e\n\t\u003c/ol\u003e\u003cp\u003eThe combination of 2) and 3) allows you to leave the Music Room and change the music mode in the Option menu to listen to the same track in the other version, without the game changing back to the title screen theme. 4), however, might cause the PMD and MMD drivers to play garbage for a short while if the music data is loaded from a slow storage device that takes longer than a single period of the OPN timer to fill the driver's song buffer. Probably not worth mentioning anymore though, now that people no longer try fitting PC-98 Touhou games on floppy disks.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe comment text files use another fixed-size plaintext format, just like \u003ca href=\"/blog/2023-11-01#th02-2023-11-01\"\u003e📝 TH02's in-game dialog system\u003c/a\u003e or \u003ca href=\"/blog/2023-11-01#final-2023-11-01\"\u003e📝 TH03's win messages\u003c/a\u003e:\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eExactly 40 (TH02/TH03) / 38 (TH04/TH05) visible bytes per line,\u003c/li\u003e\n\t\t\u003cli\u003epadded with 2 bytes that can hold a CR/LF newline sequence for easier editing.\u003c/li\u003e\n\t\t\u003cli\u003eEvery track starts with a title line that mostly just duplicates the names from the hardcoded tracklist,\u003c/li\u003e\n\t\t\u003cli\u003efollowed by a fixed 19 (TH02/TH03/TH04) / 9 (TH05) comment lines.\u003c/li\u003e\n\t\u003c/ul\u003e\u003cp\u003eIn TH04 and TH05, lines can start with a semicolon (\u003ccode\u003e;\u003c/code\u003e) to prevent them from being rendered. This is purely a performance hint, and is visually equivalent to filling the line with spaces.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eAll in all, the quality of the code is even slightly below the already poor standard for PC-98 Touhou: More VRAM page copies than necessary, conditional logic that is nested way too deeply, a distinct avoidance of state in favor of loops within loops, and – of course – a couple of \u003ccode\u003egoto\u003c/code\u003es to jump around as needed. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIn TH05, this gets so bad with the scrolling and game-changing tracklist that it all gives birth to a wonderfully obscure inconsistency: When pressing both  ⬆️/⬇️ and ⬅️/➡️ at the same time, the game first processes the vertical input and then the horizontal one in the next frame, making it appear as if the latter takes precedence. \u003ci\u003eExcept\u003c/i\u003e when the cursor is highlighting the first (⬆️ ) or 12\u003csup\u003eth\u003c/sup\u003e (⬇️ ) element of the list, \u003ci\u003eand\u003c/i\u003e said list element is not the first track (⬆️ ) or the quit option (⬇️ ), in which case the horizontal input is ignored. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/p\u003e\u003cfigure style=\"width: 320px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2024-02-03-TH05-Music-Room-input-quirks.webp?81cc6b9f\" preload=\"none\" controls loop width=\"320\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"650\" style=\"aspect-ratio: 320 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2024-02-03-TH05-Music-Room-input-quirks.avi?abab1ac8\"\u003e\u003csource src=\"/blog/static/video/av1/2024-02-03-TH05-Music-Room-input-quirks.webm?6bd03cb4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2024-02-03-TH05-Music-Room-input-quirks.webm?d90b708e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2024-02-03-TH05-Music-Room-input-quirks.webm?d79f86fe\" type=\"video/webm\"\u003eVideo demonstrating the arrow key input quirks in TH05's Music Room. \u003ca href=\"/blog/static/video/zmbv/2024-02-03-TH05-Music-Room-input-quirks.avi?abab1ac8\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd that's all the Music Rooms! The \u003ccode\u003eOP.EXE\u003c/code\u003e binaries of TH04 and especially TH05 are now very close to being 100% RE'd, with only the respective High Score menus and TH04's title animation still missing. As for actual \u003ci\u003ecompletion\u003c/i\u003e though, the finalization% metric is more relevant as it also includes the ZUN Soft logo, which I RE'd on paper but haven't decompiled. I'm \u003ca href=\"/blog/2023-11-30#main-2023-11-30\"\u003e📝 still\u003c/a\u003e hoping that this will be the final piece of code I decompile for these two games, and that no one pays to get it done earlier… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003chr id=\"cong-2024-02-03\"\u003e\u003cp\u003e\n\tFor the rest of the second push, there was a specific goal I wanted to reach for the remaining \u003ci\u003eanything\u003c/i\u003e budget, which was blocked by a few functions at the beginning of TH04's and TH05's \u003ccode\u003eMAINE.EXE\u003c/code\u003e. In another anticlimactic development, this involved yet another way too early decompilation of a \u003ccode\u003emain()\u003c/code\u003e function…\u003cbr\u003e\n\tGenerally, this \u003ccode\u003emain()\u003c/code\u003e function just calls the top-level functions of all other ending-related screens in sequence, but it also handles the TH04-exclusive congratulating \u003cq\u003eAll Clear\u003c/q\u003e images within itself. After a 1CC, these are an additional reward on top of the Good Ending, showing the player character wearing a different outfit depending on the selected difficulty. On Easy Mode, however, the Good Ending is unattainable because the game always ends after Stage 5 with a Bad Ending, but ZUN still chose to show the \u003cspan lang=\"ja\"\u003e\u003cq\u003eEASY ALL CLEAR!!\u003c/q\u003e\u003c/span\u003e image in this case, regardless of how many continues you used.\u003cbr\u003e\n\tWhile this might seem inconsistent with the other difficulties, it is consistent within Easy Mode itself, as the enforced Bad Ending after Stage 5 also doesn't distinguish between the number of continues. Also, \u003cspan lang=\"ja\"\u003e\u003cq\u003eTry to Normal Rank!!\u003c/q\u003e\u003c/span\u003e could very well be ZUN's roundabout way of implying \"because this is how you avoid the Bad Ending\".\n\u003c/p\u003e\u003cp\u003e\n\tWith that out of the way, I was finally able to separate the VRAM text renderer of TH04 and TH05 into its own assembly unit, \u003ca href=\"/blog/2021-04-23\"\u003e📝 finishing the technical debt repayment project that I couldn't complete in 2021 due to assembly-time code segment label arithmetic in the data segment\u003c/a\u003e. This now allows me to translate this undecompilable self-modifying mess of ASM into C++ for the non-ASCII translation project, and thus unify the text renderers of all games and enhance them with support for Unicode characters loaded from a bitmap font. As the final finalized function in the \u003ccode\u003eSHARED\u003c/code\u003e segment, it also allowed me to remove 143 lines of particularly ugly segmentation workarounds 🙌\n\u003c/p\u003e\u003chr id=\"th02-pi-2024-02-03\"\u003e\u003cp\u003e\n\tThe remaining \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e6\u003c/sub\u003eth of the second push provided the perfect occasion for some light TH02 PI work. The global boss position and damage variables represented some equally low-hanging fruit, being easily identified global variables that aren't part of a larger structure in this game. In an interesting twist, TH02 is the only game that uses an increasing damage value to track boss health rather than decreasing HP, and also doesn't internally distinguish between bosses and midbosses as far as these variables are concerned. Obviously, there's quite a bit of state left to be RE'd, not least because Marisa is doing her own thing with a bunch of redundant copies of her position, but that was too complex to figure out right now.\n\u003c/p\u003e\u003cp\u003e\n\tAlso doing their own thing are the Five Magic Stones, which need five positions rather than a single one. Since they don't move, the game doesn't have to keep \u003ca href=\"/blog/2023-03-30\"\u003e📝 separate position variables for both VRAM pages\u003c/a\u003e, and can handle their positions in a much simpler way that made for a nice final commit.\u003cbr\u003e\n\tAnd for the first time in a long while, I quite like what ZUN did there!\n\tNot only are their positions stored in an array that is indexed with a consistent ID for every stone, but these IDs also follow the order you fight the stones in: The two inner ones use 0 and 1, the two outer ones use 2 and 3, and the one in the center uses 4. This might look like an odd choice at first because it doesn't match their horizontal order on the playfield. But then you notice that ZUN uses this property in the respective phase control functions to iterate over only the subrange of active stones, and you realize how brilliant it actually is.\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-child-switcher\u003e\n\t\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAABcBAMAAACVYn9sAAAAIVBMVEWZiIj///+qqv+7VbsimSJmZmb/AAARZhGIAIiqAAAAAABs6OBuAAADSUlEQVR42u2aMWsbQRCF15g9klTeE7i/VZMy1mHQP0h7JLyArjo39xfSGkQgZZor5MrNQny/MoO0OIGFmw0DYw72s9HaMLx5syMdr5ApFAqFQqFQKBQKhf/jum0/mQws0GXrKdLeOef2JgsgW0+N6ztHbB4Nx9UVvaDL1tPi3C/nym5ukhUweipcOyJeGb8AY9Fl6+nQOufPv3t+AYQFcvXUFlDTj2tc/bi8gHgCXa6e2idgs2sd9c27MgC5ekoD1HW7a9umEQwg0BNDy940ztV3db2VDZDqKX0EmtZ571pquPCmtd0/A3S5ejoDUK+mpq6yAVI9rQHq2tfONb7JG8DGAXg9pQGazA0gc4BGewP+9T3rlweIrkF0eXpKAzTxqZE3gAXRCfQSpPl9533dttu28f6jWQDoon8gX0/uD+AHcBtq6L3fLwq9EiDQS2Wl+f1InRrX0Os2b4C+h0Qv9SfM7+92PtI+Lq87EkIn0Uv9CfP7cPQEf2ED4v33kOml/mT5fYxXtqULq5bqLIhAdKaS6KX+RPmdOO7O/fbU3CwRPwCgOpFe6k+UHi1+H1ti/6N/Xq4LuEB1Ir3Un6BgHED8JIAwM3WRMOfqKQwwWADTBODU9/2zSl3qTxJ/8e1pmk4ADuFzmHXqeH/5A9j58Ov9hNOX+/7pO12ZSh3vLz+/o7M4TZi+3oeXEMKsUpf6E+R34HACpp54mmeFOsYfm99TDhMwhTDHdgp1qT9Bfq+qcfjwcprnZzOOg05d6k+Q36tzl/Hy96BSl/qT5HfqR03PXelUqUv9CfJ7bDUO8T+NutSfIL8Psa+Jp0Zd6k+Q36v4vo2NK4W61J8ov18axs0blTreH5/fU2JHhTrWH5/fU+IjT6GO9cfnd31Yf4Jcrg7vj8/l+rD+BLlcE94fn8vfHt4fn8v14fwJcrkyvD8+l+vD+xPkcm14f3wu14f1J8jl6vD++FyuD+tPkMu14f3xuVwfgb+3nkDq7+0foay/8q3XQqFQKPwFK8cAMwGs9TRd75x7sGGtp0F/M45XFms9aQO3ZriyYZ3nZQNVRZOs9TQdrWJ4sGGtpwGtwjxYrPWkDbjb0dmw1tMAhgDWepoOZ8Jaz9VHiT/BlZATUyNFOAAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Stones 0 and 1\"\n\t\talt=\"Screenshot of TH02's Five Magic Stones, with the first two (both internally and in the order you fight them in) alive and activated\"\n\t\tclass=\"active\"\n\t\u003e\n\t\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAABcBAMAAACVYn9sAAAAIVBMVEWZiIj///+qqv+7VbsimSJmZmb/AAARZhGIAIiqAAAAAABs6OBuAAAC90lEQVR42u2asW7bMBCG6QAUqk7mIiAP0mfoKrS4AuYkLof6Cbp2KVBv8SAjzOSFQKNX7FIyJpJNvwIIpxLglyAXwx/A/3CWdINVpVKpVCqVSqVSqbwPTdSv7glDtLonx26XTuxX98TY79OBtLYnOwClqV/bEx2A0kSrezLsciXqV/dEISIBD3Cn/itqAwvQvXqjX9urE3gHWsTDDWwfXHwCfS6CHm6gjEFgWu8fgJIfmJoSMt7yfP5sjHlEDbwSSMRbnq89m8j9z4UNWEsi3vJ86X08Ak2ZEHpJD+drTQSPYKAXrLUk6uF83hiOv2gErCkSIr1qZDycLzfYxR/jTIevArKWSCmW83C+c5rOxZvozY5AU6Ab9irj4XxZ6Dp/8d65uQZ4oFfCJOAtz2eMu3exvXPXHWcuOU1E40hE3lp7FfNwvjYK3jAbH4WZi4C+PY2jJ6JD+BwmEQ/ny0J8z3XRmmtAT4eHdiT/5ZN9+mHtVcDD+bLQdZwucnZzDVCvyY80fv0UnkMIk5SH87UOdJghOnii0UaepknMw/lafv2McRRmOIxEYwhTOk7Mw/ladvkqn2+gaXj4+Oyn6aqYBzkP57swd94fvWP+rmYOfDmFb/8PMh7OlwVzHwVmnnsS85AOTX9TlfNwvhMzO+OY+TjTQD6Kh/xKwFua78OFM37mEshDZpWrgLc033DiCBiAavLndsgvBbyl+Ti3eIwNNmqWhjnPXMJbnu+UjKN/jLKC5BPFPJxP05+Tjzz+sleFyLc8KQ/ny3v57wjYy2XA+fBevj04H97Ltwfnw3u5PDgf3su3B+fDe/n24Hx4L98enA/v5fLgfHgv3x6cD+/l24Pz4b1cHpwP7+Xbg/PhvXx7cD68l2/P8nzA2Bicb/tbKMxXv/VaqVQqlTeocO5uLShVbO2tMcbpUGpVZPfMO02lVtXbTg07HUqtaQJNEzsptKYJ7HlwOpRaFdlOKaep1JruQh0bHUqtqthnQK53f28v+lJr8avEPwqse/10QYvbAAAAAElFTkSuQmCC\"\n\t\tdata-title=\"Stones 2 and 3\"\n\t\talt=\"Screenshot of TH02's Five Magic Stones, with the second two (both internally and in the order you fight them in) alive and activated\"\n\t\u003e\n\t\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAABcBAMAAACVYn9sAAAAIVBMVEWZiIj///+qqv+7VbsimSJmZmb/AAARZhGIAIiqAAAAAABs6OBuAAADfElEQVR42u2asWrcQBCGVwIJkkqjc1yf5jC4Pa39ACGGtIfDBE6VnEKvkAfwC4SAinN1KbawnjKbO8UuFjQyA7sW7Fd4bPiZ+WdXp/sLq0gkEolEIpFIJBJ5GxnRTs0gAShm9/MMkWKBE0qt5/XzR5LYH9yZrVUCqxu9hQLTmf28URT8kaXpGlb67s5ukK7n9fN7ASpjjgyTlb6Biy96W6xn9vN6ASpjjmwNWn8GuNMaZvfzQzJW5s2RrE4LXOjttpjdzys0fWSgVxpKgO0WQNAv4AKwwhIRLAtdoMQKEauyel8LZDs1QpMPbYKIgNougIiFoF+gBdIENQJuEEvUWKzf4QLZ5EBMSl2VjxtEwLoqcGY/Lzlu1sA0KWu4rrFGLOtq6gbI+wLjFGJuABEfTwugxgJn9PO7gK3cZwCvNWKNG/3yGeD7+cj5VnJWst8Dj/Wm1nYBrN7ST+6PE7xgmAXQomuNWAn6uXJpLqf/NA33RYY11rquACT9XH/CXJ7RiDHTYa5E1LpGDVBI+rn+hLm8Hc+LO7AWADd1jQiQivq5/mS5vMvIYiw7lU/ofiRQIm4QocBc0s/1J83l4wNLdviEKl0DnNNo6uiYfnJ/08+soTPNUU1whX/gxCc8yvq5/gSCrqUXzDCluycisLzq+H4eFmgzIup7Ijo0TXP0onP9SWItfX/q+wMR7c1XM3A6xCtWx/fj/c1fIBv2vz70dLi/bZ5+2iNjdL8dHdNPuAAfa2mX0aGn/tuteTbGDF50rj9BLifaH4j6xvI0DH50rj9RLt/3RL0xwzjOg871J8jled61H58Pw3BUXdf60bn+BLk8P03pzr+3XnSuP0ku/zcvP5+VrV50rj9BLh9Hde34lw+d60+Qy9txrhqrD53rT5DL8/G5HQfnHnSuP2kutwOnXhhyHe+Pz+UszESZjvUny+XcK0+u4/zxuTwsvD8+lweF98fn8rDw/vhcHhTeH5/Lw8L6Y3N5YDh/XC4PDeuPz+VB4f3xuTwsrD8+l4eF98fn8qDw/vhcHhbWH5/Lw8L6Y3J5cAT+Qm8g9Rf+Fcr6i//1GolEIpFXaOEoosFCtNSqdg0APGRmqVVRU3RdktFSq72BS9UmmVlmPd9AnttNllrVzl5F+5CZpVZF9irUQ0ZLrfYG4LKDzCy1KiJlIVpqVTs6YZZaFx8l/gJPZ5VZZhyRagAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Stone 4\"\n\t\talt=\"Screenshot of TH02's Five Magic Stones, with the last one (both internally and in the order you fight them in) alive and activated\"\n\t\u003e\n\t\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis seems like a really basic thing to get excited about, especially since the rest of their data layout sure isn't perfect. Splitting each piece of state and even the individual X and Y coordinates into \u003ca href=\"https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_Arrays\"\u003eseparate 5-element arrays\u003c/a\u003e is still counter-productive because the game ends up paying more memory and CPU cycles to recalculate the element offsets over and over again than this would have ever saved in cache misses on a 486. But that's a minor issue that could be fixed with a few regex replacements, not a misdesigned architecture that would require a full rewrite to clean it up. Compared to the hardcoded and bloated mess that was \u003ca href=\"/blog/2022-08-08\"\u003e📝 YuugenMagan's five eyes\u003c/a\u003e, this is definitely an improvement worthy of the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e tag. The first actual one in two years, and a welcome change after the Music Room!\n\u003c/p\u003e\u003cp\u003e\n\tThese three pieces of data alone yielded a whopping 5% of overall TH02 PI in just \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e6\u003c/sub\u003eth of a push, bringing that game comfortably over the 60% PI mark. \u003ccode\u003eMAINE.EXE\u003c/code\u003e is guaranteed to reach 100% PI before I start working on the non-ASCII translations, but at this rate, it might even be realistic to go for 100% PI on \u003ccode\u003eMAIN.EXE\u003c/code\u003e as well? Or at least technical position independence, without the false positives.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Shuusou Gyoku SC-88Pro BGM. It's going to be wild.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-03-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2024-02-03T08:03:19Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-11-30",
      "url": "https://rec98.nmlgc.net/blog/2023-11-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-02-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-11-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-11-30\"\u003e\u003ctime datetime=\"2023-11-30T23:56:14Z\"\u003e2023-11-30 23:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0262\"\u003eP0262\u003c/a\u003e\n\t\t\tDecompilation (TH04/TH05 main/option menu)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ae2fc28...741d889\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0263\"\u003eP0263\u003c/a\u003e\n\t\t\tDecompilation (TH04/TH05 first-launch sound setup menu + TH05 title screen animation)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/741d889...46cd6e7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAnd once again, the Shuusou Gyoku task was too complex to be satisfyingly  solved within a single month. Even just \u003ci\u003efinding\u003c/i\u003e provably correct loop sections in both the original and arranged MIDI files required some rather involved detection algorithms. I could have just defined what \u003ci\u003esounded\u003c/i\u003e like correct loops, but the results of these algorithms were quite surprising indeed. Turns out that not even Seihou is safe from ZUN quirks, and some tracks technically loop much later than you'd think they do, or don't loop at all. And since I then wanted to put these MIDI loops back into the game to ensure perfect synchronization between the recordings and MIDI versions, I ended up rewriting basically \u003ci\u003eall\u003c/i\u003e the MIDI code in a cross-platform way. This rewrite also uncovered \u003ca href=\"https://twitter.com/ReC98Project/status/1724916760053211163\"\u003ea pbg bug that has traveled from Shuusou Gyoku into Windows Touhou\u003c/a\u003e, where it survived until ZUN ultimately removed all MIDI code in \u003cspan class=\"hovertext\" title=\"Yes, TH10 still has MIDI code, even though no publicly released version included a MIDI soundtrack.\"\u003eTH11\u0026nbsp;(!)\u003c/span\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tFortunately, the backlog still had enough general PC-98 Touhou funds that I could spend on picking some soon-important low-hanging fruit, giving me something to deliver for the end of the month after all. TH04 and TH05 use almost identical code for their main/option menus, so decompiling it would make number go up quite significantly and the associated blog post won't be that long…\n\u003c/p\u003e\u003cp\u003e\n\tWait, what's this, a bug report from \u003ca href=\"https://touhou-memories.com\"\u003etouhou-memories\u003c/a\u003e concerning the website?\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eTab switchers tended to break on certain Firefox versions, and\u003c/li\u003e\n\t\u003cli\u003evideo playback didn't work on Microsoft Edge at all?\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThose are definitely some high-priority bugs that demand immediate attention.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#edge-2023-11-30\"\u003eMicrosoft Edge's anti-support of AV1\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#main-2023-11-30\"\u003eTH04/TH05's main/option menu\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#setup-2023-11-30\"\u003eTH04/TH05's first-launch sound setup menu\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#title5-2023-11-30\"\u003eTH05's title animation ☯️\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr\u003e\u003cp id=\"edge-2023-11-30\"\u003e\n\tThe tab switcher issue was easily fixed by replacing the previous \u003ccode\u003ez-index\u003c/code\u003e trickery with a more robust solution involving the \u003ccode\u003ehidden\u003c/code\u003e attribute. The second one, however, is much more aggravating, because video playback on Edge has been broken ever since I \u003ca href=\"/blog/2022-10-31\"\u003e📝 switched the preferred video codec to AV1\u003c/a\u003e.\u003cbr\u003e\n\tThis goes so far beyond not supporting a specific codec. Usually, unsupported codecs aren't \u003ci\u003esupposed\u003c/i\u003e to be an issue: As soon as you start using the HTML \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e tag, you'll learn that not every browser supports all codecs. And so you set up an encoding pipeline to serve each video in a mix of new and ancient formats, put the \u003ccode\u003e\u0026lt;source\u0026gt;\u003c/code\u003e tag of the most preferred codec first, and rest assured that browsers will fall back on the best-supported option as necessary. Except that Edge doesn't even \u003ci\u003etry\u003c/i\u003e, and insists on staying on a non-playing AV1 video. 🙄\n\u003c/p\u003e\u003cp\u003e\n\tThe \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter\"\u003e\u003ccode\u003ecodecs\u003c/code\u003e parameter for the \u003ccode\u003e\u0026lt;source\u0026gt; type\u003c/code\u003e attribute\u003c/a\u003e was the first potential solution I came across. Specifying the video codec down to the finest encoding details right in the HTML markup sounds like a good idea, similar to specifying sizes of images and videos to prevent layout reflows on long pages during the initial page load. So why was this the first time I heard of this feature? The fact that there isn't a simple \u003ccode\u003effprobe -show_html_codecs_string\u003c/code\u003e command to retrieve this string might already give a clue about how useful it is in practice. Instead, you have to \u003ca href=\"https://jakearchibald.com/2022/html-codecs-parameter-for-av1/\"\u003emanually piece the string together by \u003ccode\u003egrep\u003c/code\u003eping your way through all of a video's metadata\u003c/a\u003e…\u003cbr\u003e\n\t…and then it \u003ci\u003estill\u003c/i\u003e doesn't change anything about Edge's behavior, even when also specifying the string for the VP9 and VP8 sources. Calling the \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType\"\u003einfamously ridiculous \u003ccode\u003eHTMLMediaElement.canPlayType()\u003c/code\u003e method\u003c/a\u003e with a representative parameter of \u003ccode\u003e\"video/webm; codecs=av01.1.04M.08.0.000.01.13.00.0\"\u003c/code\u003e explains why: Both the AV1-supporting Chrome and Edge return \u003ccode\u003e\"probably\"\u003c/code\u003e, but only the former can actually play this format. 🤦\n\u003c/p\u003e\u003cp\u003e\n\tBut wait, there is an \u003ca href=\"https://apps.microsoft.com/detail/9MVZQVXJBQ9V\"\u003eAV1 video extension in the Microsoft Store\u003c/a\u003e that would add support to any unspecified \u003cq\u003efavorite video app\u003c/q\u003e. Except that it \u003ca href=\"https://www.reddit.com/r/AV1/comments/16mmm35/av1_and_edge/\"\u003estopped working inside Edge\u003c/a\u003e as of \u003ca href=\"https://caniuse.com/av1\"\u003eversion 116\u003c/a\u003e. And even if it did: If you can't query the presence of this extension via JavaScript, it might as well not exist at all.\u003cbr\u003e\n\tNot to mention that the \u003cq\u003efavorite video app\u003c/q\u003e part is obviously a lie as a lot of widely preferred Windows video apps are bundled with their own codecs, and have probably long supported AV1.\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, there's no way around the utter desperation move of removing the AV1 \u003ccode\u003e\u0026lt;source\u0026gt;\u003c/code\u003e for Edge users. Serving each video in two other formats means that we can at least do \u003ci\u003esomething\u003c/i\u003e here – try visiting \u003ca href=\"https://github.com/nmlgc/ReC98/releases/tag/P0234-1\"\u003ethe GitHub release page of the P0234-1 TH01 Anniversary Edition build\u003c/a\u003e in Edge and you also don't get to see anything, because that video uses AV1 and GitHub understandably doesn't re-encode every uploaded video into a variety of old formats.\u003cbr\u003e\n\tJust for comparison, I tried both that page and the ReC98 blog on an old Android 6 phone from 2014, and even that phone picked and played the AV1 videos with the latest available Chrome and Firefox versions. This was the phone whose available Firefox version didn't support VP9 in 2019, which was my initial reason for adding the VP8 versions. Looks like it's finally time to drop those… 🤔 Maybe in the far future once I start running out of space on this server.\n\u003c/p\u003e\u003cp\u003e\n\tRemoving the \u003ccode\u003e\u0026lt;source\u0026gt;\u003c/code\u003e tags can be done in one of two places:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eserver-side, detecting Edge via the \u003ccode\u003eUser-Agent\u003c/code\u003e header, or \u003c/li\u003e\n\t\u003cli\u003eclient-side, using \u003ca href=\"https://vaihe.com/quick-seo-tips/using-av1-video-format-as-source-in-video/\"\u003e\u003ccode\u003enavigator.userAgentData.brands\u003c/code\u003e\u003c/a\u003e.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tI went with 2) because more dynamic server-side code would only move us further away from static site generation, which would make a lot of sense as the next evolutionary step in the architecture of this website. The client-side solution is much simpler too, and we can defer the deletion until a user actually hovers over a specific video.\u003cbr\u003e\n\tAnd while we're at it, let's also add a popup complaining about this whole state of affairs. Edge is heavily marketed inside Windows as \"the modern browser recommended by Microsoft\", and you sure wouldn't expect low-quality chroma-subsampled VP9 from such a tagline. With such a level of anti-support for AV1, Edge users deserve to know exactly what's going on, especially since this post also explains what they will encounter on other websites.\n\u003c/p\u003e\u003cfigure\u003e\n\t\n\t\u003cimg\n\t\tstyle=\"width: 640px;\"\n\t\tsrc=\"/blog/static/2023-11-30-Edge-AV1-popup.png?dbc673d5\"\n\t\talt='A popup on top of a ReC98 blog video, showing the caption \"⚠️ Edge does not support AV1, falling back on low-quality video…\"'\n\t\u003e\n\t\u003cfigcaption\u003eThat's the polite way of putting it.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp id=\"main-2023-11-30\"\u003e\n\tAlright, where was I? For TH01, the main menu was the last thing I decompiled before the 100% finalization mark, so it's rather anticlimactic to already cover the TH04/TH05 one now, with both of the games still being very far away from 100%, just because people will soon want to translate the description text in the bottom-right corner of the screen. But then again, the ZUN Soft logo animation would make for an even nicer final piece of decompiled code, especially since the bouncing-ball logo from TH01, TH02, and TH03 was \u003ca href=\"https://github.com/nmlgc/ReC98/commit/f861b0a5c37ef645cd88949c4f41d7e81a65f80b\"\u003ethe very first decompilation I did, all the way back in 2015\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tThe code quality of ZUN's VRAM-based menus has barely increased between TH01 and TH05. Both the top-level and option menu still need to know the bounding rectangle of the other one to unblit the right pixels when switching between the two. And since ZUN sure loved hardcoded and copy-pasted numbers in the PC-98 days, the coordinates both tend to be excessively large, and excessively wrong. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Luckily, each menu item comes with its own correct unblitting rectangle, which avoids any graphical glitches that would otherwise occur.\u003cbr\u003e\n\tAs for actual observable quirks and bugs, these menus only contain one of each, and both are exclusive to TH04:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eQuitting out of the Music Room moves the cursor to the \u003ci\u003eStart\u003c/i\u003e option. In TH05, it stays on \u003ci\u003eMusic Room\u003c/i\u003e.\u003c/li\u003e\n\t\u003cli\u003eChanging the \u003ci\u003eS.E.\u003c/i\u003e mode seems to do nothing within TH04's menus, and would only take effect if you also change the \u003ci\u003eMusic\u003c/i\u003e mode afterward, or launch into the game.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-30-TH04-SE-change.webp?3364f16f\" preload=\"none\" controls data-title=\"TH04\" loop data-active width=\"640\" height=\"400\" data-fps=\"2\" data-frame-count=\"9\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-11-30-TH04-SE-change.avi?4d64fc82\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-30-TH04-SE-change.webm?66b17852\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-30-TH04-SE-change.webm?76041c63\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-30-TH04-SE-change.webm?f1196f96\" type=\"video/webm\"\u003eVideo demonstrating how changing the sound effect mode in TH04's Option menu doesn't take immediate effect. \u003ca href=\"/blog/static/video/zmbv/2023-11-30-TH04-SE-change.avi?4d64fc82\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-30-TH05-SE-change.webp?d580a9f7\" preload=\"none\" controls data-title=\"TH05\" loop width=\"640\" height=\"400\" data-fps=\"2\" data-frame-count=\"9\" style=\"aspect-ratio: 640 / 400\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-11-30-TH05-SE-change.avi?e126b820\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-30-TH05-SE-change.webm?e10df57d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-30-TH05-SE-change.webm?f92df2bc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-30-TH05-SE-change.webm?a9d0ff7f\" type=\"video/webm\"\u003eVideo demonstrating how TH05 fixed TH04's sound effect mode change bug. \u003ca href=\"/blog/static/video/zmbv/2023-11-30-TH05-SE-change.avi?e126b820\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eAnd yes, these videos do have a frame rate of 2 FPS.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow that 100% finalization of their \u003ccode\u003eOP.EXE\u003c/code\u003e binaries is within reach, all this bloat made me think about the viability of a \u003ca href=\"/blog/2022-03-05\"\u003e📝 single-executable build\u003c/a\u003e for TH04's and TH05's \u003ccode\u003edebloated\u003c/code\u003e and \u003ccode\u003eanniversary\u003c/code\u003e versions. It would be really nice to have such a build ready before I start working on the non-ASCII translations – not just because they will be based on the \u003ccode\u003eanniversary\u003c/code\u003e branch by default, but also because it would significantly help their development if there are 4 fewer executables to worry about.\u003cbr\u003e\n\tHowever, it's not as simple for these games as it was for TH01. The unique code in their \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINE.EXE\u003c/code\u003e binaries is much larger than Borland's easily removed C++ exception handler, so I'd have to remove a lot more bloat to keep the resulting single binary at or below the size of the original \u003ccode\u003eMAIN.EXE\u003c/code\u003e. But I'm sure going to try.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"setup-2023-11-30\"\u003e\n\tSpeaking of code that can be debloated for great effect: The second push of this delivery focused on the first-launch sound setup menu, whose BGM and sound effect submenus are almost complete code duplicates of each other. The \u003ccode\u003edebloated\u003c/code\u003e branch could easily remove more than half of the code in there, yielding another ≈800 bytes in case we need them.\u003cbr\u003e\n\tIf hex-editing \u003ccode\u003eMIKO.CFG\u003c/code\u003e is more convenient for you than deleting that file, you can set its first byte to \u003ccode\u003eFF\u003c/code\u003e to re-trigger this menu. Decompiling this screen was not only relevant now because it contains text rendered with font ROM glyphs and it would help dig our way towards more important strings in the data segment, but also because of its visual style. I can imagine many potential mods that might want to use the same backgrounds and box graphics for their menus.\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-30-TH04-Sound-setup.png?9ee5711f\"\n\t\tdata-title=\"TH04, BGM\"\n\t\talt=\"TH04's first-launch sound setup menu, showing the BGM mode selection\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-30-TH05-Sound-setup.png?b649287c\"\n\t\tdata-title=\"TH05, sound effects\"\n\t\talt=\"TH05's first-launch sound setup menu, showing the sound effect mode selection\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eHow about an initial language selection menu in the same style?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith the two submenus being shown in a fixed sequence, there's not a lot of room for the code to do anything wrong, and it's even more identical between the two games than the main menu already was. Thankfully, ZUN just reblits the respective options in the new color when moving the cursor, with no \u003ca href=\"/blog/2021-11-08\"\u003e📝 palette tricks\u003c/a\u003e. TH04's background image only uses 7 colors, so he could have easily reserved 3 colors for that. In exchange, the TH05 image gets to use the full 16 colors with no change to the code.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"title5-2023-11-30\"\u003e\n\tRounding out this delivery, we also got TH05's rolling Yin-Yang Orb animation before the title screen… and it's just more bloat and landmines on a smaller scale that might be noticeable on slower PC-98 models. In total, there are three unnecessary inter-page copies of the entire VRAM that can easily insert lag frames, and two minor page-switching landmines that can potentially lead to tearing on the first frame of the roll or fade animation. Clearly, ZUN did not have smoothness or code quality in mind there, as evidenced by the fact that this animation simply displays 8 .PI files in sequence. But hey, a short animation like this is \u003ca href=\"/blog/2022-08-11\"\u003e📝 another perfectly appropriate place for a quick-and-dirty solution if you develop with a deadline\u003c/a\u003e.\u003cbr\u003e\n\tAnd that's 1.30% of all PC-98 Touhou code finalized in two pushes! We're slowly running out of these big shared pieces of ASM code…\n\u003c/p\u003e\u003cp\u003e\n\tI've been neglecting TH03's \u003ccode\u003eOP.EXE\u003c/code\u003e quite a bit since it simply doesn't contain any translatable plaintext outside the Music Room. All menu labels are \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e, and even the character selection menu displays its monochrome character names using the 4-plane sprites from \u003ccode\u003eCHNAME.BFT\u003c/code\u003e. Splitting off half of its data into a separate .ASM file was more akin to getting out a jackhammer to free up the room in front of the third remaining Music Room, but now we're there, and I can decompile all three of them in a natural way, with all referenced data.\u003cbr\u003e\n\tNext up, therefore: Doing just that, securing another important piece of text for the upcoming non-ASCII translations and delivering another big piece of easily finalized code. I'm going to work full-time on ReC98 for almost all of December, and delivering that and the Shuusou Gyoku SC-88Pro recording BGM back-to-back should free up about half of the slightly higher cap for this month.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2024-02-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-11-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-11-30T23:56:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-11-01",
      "url": "https://rec98.nmlgc.net/blog/2023-11-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-09-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-11-01\"\u003e\u003ctime datetime=\"2023-11-01T23:58:05Z\"\u003e2023-11-01 23:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0258\"\u003eP0258\u003c/a\u003e\n\t\t\tDecompilation (TH04/TH05 stage dialogs, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5876755...e8a0b3e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0259\"\u003eP0259\u003c/a\u003e\n\t\t\tDecompilation (TH04/TH05 stage dialogs, part 2/2) + TH02 RE (Stage dialog preparations)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e8a0b3e...dfaa3c6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0260\"\u003eP0260\u003c/a\u003e\n\t\t\tDecompilation (TH02 stage dialogs, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dfaa3c6...ed9ee93\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0261\"\u003eP0261\u003c/a\u003e\n\t\t\tDecompilation (TH02 stage dialogs, part 2/2 + TH03 win messages + TH04/TH05 MIKO.CFG + TH04 game startup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ed9ee93...ae2fc28\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous], Yanga, \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dialog\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The in-game dialog system.\"\u003edialog\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/stage\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The main scrolling portions of gameplay in TH02, TH04, and TH05. Mostly rendered using tile maps.\"\u003estage\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH02\u0026#39;s Stage 5 boss.\"\u003emima-th02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\n\n\u003cstyle\u003e\n\t#facetiles-2023-11-01,\n\t#facetiles-2023-11-01 img#miko_k-grid-2023-11-01 {\n\t\twidth: 544px;\n\t}\n\n\t#bugs-2023-11-01,\n\t#bugs-2023-11-01 img {\n\t\twidth: 480px;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tAnd we're back to PC-98 Touhou for a brief interruption of the ongoing \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/42\"\u003eShuusou Gyoku Linux port\u003c/a\u003e.\n\tLet's clear some of the Touhou-related progress from the backlog, and use\n\tthe unconstrained nature of these contributions to prepare the\n\t\u003ca href=\"/blog/2023-07-28\"\u003e📝 upcoming non-ASCII translations commissioned\tby Touhou Patch Center\u003c/a\u003e.\n\tThe current budget won't cover all of my ambitions, but it would at least be\n\tnice if all text in these games was feasibly translatable by the time I\n\tofficially start working on that project.\n\u003c/p\u003e\u003cp\u003e\n\tAt a little over 3 pushes, it might be surprising to see that this took\n\tlonger than the\n\t\u003ca href=\"/blog/2022-11-30\"\u003e📝 TH03/TH04/TH05 cutscene system\u003c/a\u003e. It's\n\tobvious that TH02 started out with a different system for in-game dialog,\n\tbut while TH04 and TH05 \u003ci\u003elook\u003c/i\u003e identical on the surface, they only\n\tactually share 30% of their dialog code. So this felt more like decompiling\n\t2.4 distinct systems, as opposed to one identical base with tons of\n\tgame-specific differences on top.\n\u003c/p\u003e\u003cp\u003e\n\tThe table of contents was pretty popular last time around, so let's have\n\tanother one:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#th04-2023-11-01\"\u003eOverview of TH04's dialog system\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th05-2023-11-01\"\u003eChanges introduced in TH05\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#ref-2023-11-01\"\u003eCommand reference for the TH04 and TH05 systems\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-2023-11-01\"\u003eOverview of TH02's dialog system\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-face-2023-11-01\"\u003eTH02's face portrait images\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-start-2023-11-01\"\u003eBugs during TH02's dialog box slide-in animation\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#th02-mima-2023-11-01\"\u003eBugs and quirks in Mima's defeat dialog (might be lore-relevant)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#final-2023-11-01\"\u003eTH03 win messages\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr\u003e\u003cp id=\"th04-2023-11-01\"\u003e\n\tLet's start with the ones from TH04 and TH05, since they are not \u003ci\u003ethat\u003c/i\u003e\n\tbroken. For TH04, ZUN started out by copy-pasting the cutscene system,\n\tcausing the result to inherit many of the caveats I already described in the\n\tcutscene blog post:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt's still a plaintext format geared exclusively toward full-width\n\tJapanese text.\u003c/li\u003e\n\t\u003cli\u003eThe parser still ignores all whitespace, forcing ASCII text into hacks\n\twith unassigned Shift-JIS lead bytes outside the second byte of a 2-byte\n\tchunk.\u003c/li\u003e\n\t\u003cli\u003eCommands are still preceded by a \u003ccode\u003e0x5C\u003c/code\u003e byte, which renders\n\tas either a \u003ccode\u003e\\\u003c/code\u003e or a \u003ccode\u003e¥\u003c/code\u003e depending on your font and\n\tinterpretation of Shift-JIS.\u003c/li\u003e\n\t\u003cli\u003eCommand parameters are parsed in exactly the same way, with all the same\n\tlimits.\u003c/li\u003e\n\t\u003cli\u003eA lot of the same script commands are identical, including 7 of them\n\tthat were not used in TH04's original dialog scripts.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThen, however, he greatly simplified the system. Mainly, this was done by\n\tmoving text rendering from the PC-98 graphics chip to the text chip, which\n\tavoids the need for any text-related unblitting code, but ZUN also added a\n\tbunch of smaller changes:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe player must advance through every dialog box by releasing any held\n\tkeys and then pressing any key mapped to a game action. There are no\n\ttimeouts.\u003c/li\u003e\n\t\u003cli\u003eThe delay for every 2 bytes of text was doubled to 2 frames, and can't\n\tbe overridden.\u003c/li\u003e\n\t\u003cli\u003eInstead of holding \u003ccode\u003eESC\u003c/code\u003e to fast-forward, pressing any key\n\twill immediately print the entire rest of a text box.\u003c/li\u003e\n\t\u003cli\u003eDialogs run in their own single-buffered frame loop, interrupting the\n\trest of the game. The other VRAM page keeps the background pixels required\n\tfor unblitting the face images.\u003c/li\u003e\n\t\u003cli\u003eAll script commands that affect the graphics layer are preceded by a\n\t1-frame delay. ZUN most likely did this because of the single-buffered\n\tnature, as it prevents tearing on the first frame by waiting for the CRT\n\tbeam to return to the top-left corner before changing any pixels.\u003c/li\u003e\n\t\u003cli\u003eBoth boxes are intended to contain up to 30 half-width characters on\n\teach of their up to 3 lines, but nothing in the code enforces these limits.\n\tThere is no support for automatic line breaks or starting new boxes.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tWhile it would seem that TH05 has no issues with ASCII \u003ccode\u003e0x20\u003c/code\u003e\n\t\tspaces, the text as a whole is still blindly processed two bytes at a\n\t\ttime, and any commands can only appear at even byte positions within a\n\t\tline. I dimmed the VRAM pixels to 25% of their original brightness to make the\n\ttext easier to read.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe same text backported to TH04, additionally demonstrating how that\n\t\tgame's dialog system inherited the whitespace skipping behavior of\n\t\tTH03's cutscene system. Just like there, ASCII \u003ccode\u003e0x20\u003c/code\u003e spaces\n\t\tonly work at odd byte positions because the game treats them as the\n\t\ttrailing byte of a full-width Shift-JIS codepoint. I don't know how\n\t\tlarge the budget for the upcoming non-ASCII translations will be, but\n\t\tI'm going to fix this even in the very basic fully static variant.\n\t\tI dimmed the VRAM pixels to 25% of their original brightness to make the\n\ttext easier to read.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH05-halfwidth.png?91a4acdf\"\n\t\tdata-title=\"TH05\"\n\t\talt=\"Demonstrating the lack of automatic line or box breaks in TH05's dialog system\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH04-halfwidth.png?b1394be9\"\n\t\tdata-title=\"TH04\"\n\t\talt=\"Demonstrating the lack of automatic line or box breaks in TH04's dialog system, in addition to its lack of support for ASCII 0x20 spaces carried over from TH03's cutscene system\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp id=\"th05-2023-11-01\"\u003e\n\tTH05 then moved from TH04's plaintext scripts to the binary\n\t\u003ccode\u003e.TX2\u003c/code\u003e format while removing all the unused commands copy-pasted\n\tfrom the cutscene system. Except for \u003ca href=\"#2023-11-01-th05-boxwipe\"\u003ea\n\tsingle additional command intended to clear a text box\u003c/a\u003e, TH05's dialog\n\tsystem only supports a strict subset of the features of TH04's system.\u003cbr\u003e\n\tThis change also introduced the following differences compared to TH04:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe game now stores the dialog of all 4 playable characters in the same\n\tfile, with a \u003ccode\u003e(4 + 1)\u003c/code\u003e-word header that indicates the byte offset\n\tand length of each character's script. This way, it can load only the one\n\tscript for the currently played character.\u003c/li\u003e\n\t\u003cli\u003eSince there is no need for whitespace in a binary format, you can now\n\tuse ASCII \u003ccode\u003e0x20\u003c/code\u003e spaces even as the first byte of a 2-byte text\n\tchunk! 🥳\u003c/li\u003e\n\t\u003cli\u003eAll command parameters are now mandatory.\u003c/li\u003e\n\t\u003cli\u003eFilenames are now passed directly by pointer to the respective game\n\tfunction. Therefore, they now need to be null-terminated, but can in turn be\n\tas long as\n\t\u003ca href=\"/blog/2022-03-27\"\u003e📝 the number of remaining bytes in the allocated dialog segment\u003c/a\u003e.\n\tIn practice though, the game still runs on DOS and shares its restriction of\n\t8.3 filenames… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eWhen starting a new dialog box, any existing text in the other box is\n\tnow colored blue.\u003c/li\u003e\n\t\u003cli\u003eThanks to ZUN messing up the return values of the command-interpreting\n\t\u003ccode\u003eswitch\u003c/code\u003e function, you can effectively use only \u003ca\n\thref=\"#2023-11-01-linebreak\"\u003eline break\u003c/a\u003e and \u003ca\n\thref=\"#2023-11-01-gaiji\"\u003egaiji\u003c/a\u003e commands in the middle of text. All other\n\tcommands do execute, but the interpreter then also treats their command byte\n\tas a Shift-JIS lead byte and places it in text RAM together with whatever\n\tother byte follows in the script.\u003cbr\u003e\n\tThis is why TH04 can and does put its \u003ca\n\thref=\"#2023-11-01-face\"\u003e\u003ccode\u003e\\=\u003c/code\u003e commands\u003c/a\u003e \u003cq\u003einto\u003c/q\u003e the boxes\n\tstarted with the \u003ccode\u003e0\u003c/code\u003e or \u003ccode\u003e1\u003c/code\u003e commands, but TH05 has to\n\tput its \u003ccode\u003e0x02\u003c/code\u003e commands before the equivalent \u003ca\n\thref=\"2023-11-01-th05-start\"\u003e\u003ccode\u003e0x0D\u003c/code\u003e\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tWriting the \u003ccode\u003e0x02\u003c/code\u003e byte to text RAM results in an \u003cimg\n\t\t\tclass=\"inline_sprite\"\n\t\t\tsrc=\"data:image/gif;base64,R0lGODlhBwAPAPABAAAAAAAAACH5BAUKAAEALAAAAAAHAA8AAAIUDB4Jacp/1nONGjSzrLtXtjke1RUAOw==\"\n\t\t\talt=\"SX\"\n\t\t\u003e character, which is simply the PC-98 font ROM's glyph for that\n\t\tShift-JIS codepoint.\u003cbr\u003e Also note how each face change is now\n\t\tpreceded by two frames of delay.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tNo problem in TH04. Note how the dialog also runs a bit faster – TH04\n\t\tonly adds the aforementioned one frame of delay to each face change, and\n\t\thas fewer two-byte chunks of text to display overall.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH05-Commands-during-text.webp?3df0ca4a\" preload=\"none\" controls data-title=\"TH05\" loop data-active width=\"384\" height=\"368\" data-fps=\"15\" data-frame-count=\"52\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH05-Commands-during-text.avi?077b802a\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH05-Commands-during-text.webm?b9d88efc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH05-Commands-during-text.webm?f4f43107\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH05-Commands-during-text.webm?0e1e454e\" type=\"video/webm\"\u003eVideo demonstrating an attempt of changing face portraits in TH05. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH05-Commands-during-text.avi?077b802a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"2\" data-title=\"0\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"10\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"16\" data-title=\"2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"26\" data-title=\"3\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"36\" data-title=\"4\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"46\" data-title=\"255\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH04-Commands-during-text.webp?058eaa13\" preload=\"none\" controls data-title=\"TH04\" loop width=\"384\" height=\"368\" data-fps=\"15\" data-frame-count=\"41\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH04-Commands-during-text.avi?5e4beb51\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH04-Commands-during-text.webm?2f8eb826\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH04-Commands-during-text.webm?2d4b1f53\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH04-Commands-during-text.webm?7ba09dc0\" type=\"video/webm\"\u003eVideo demonstrating working face portrait changes while printing dialog text in TH04. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH04-Commands-during-text.avi?5e4beb51\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"2\" data-title=\"0\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"9\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"14\" data-title=\"2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"21\" data-title=\"3\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"28\" data-title=\"4\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"35\" data-title=\"255\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFor modding these files, you probably want to use \u003ccode\u003eTXDEF\u003c/code\u003e from\n\t\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e's \u003ca\n\thref=\"http://lunarcast.net/mystictk.php\"\u003eMysticTK\u003c/a\u003e. It decodes these\n\tfiles into a text representation, and its encoder then takes care of the\n\tcharacter-specific byte offsets in the 10-byte header. This text\n\trepresentation simplifies the format a lot by avoiding all corner cases and\n\tlandmines you'd experience during hex-editing – most notably by interpreting\n\tthe box-starting \u003ca href=\"#2023-11-01-th05-start\"\u003e\u003ccode\u003e0x0D\u003c/code\u003e\u003c/a\u003e as a\n\tcommand to show text that takes a string parameter, avoiding the broken\n\tcalls to script commands in the middle of text. However, you'd still have to\n\tmanually ensure an even number of bytes on every line of text.\n\u003c/p\u003e\u003cp\u003e\n\tIn the entry function of TH05's dialog loop, we also encounter the hack that\n\tis responsible for properly handling\n\t\u003ca href=\"/blog/2020-09-21\"\u003e📝 ZUN's hidden Extra Stage replay\u003c/a\u003e. Since the\n\tdialog loop doesn't access the replay inputs but still requires key presses\n\tto advance through the boxes, ZUN chose to just \u003ca\n\thref=\"https://youtu.be/iP2ywlW2u4U?t=217\"\u003eskip the dialog altogether in the\n\tspecific case of the Extra Stage replay being active\u003c/a\u003e, and replicated all\n\tsprite management commands from the dialog script by just hardcoding\n\tthem.\u003cbr\u003e\n\tAnd you know what? Not only do I not mind this hack, but I would have\n\tpreferred it over the actual dialog system! The aforementioned \u003cq\u003esprite\n\tmanagement commands\u003c/q\u003e effectively boil down to manual memory management,\n\tdeallocating all stage enemy and midboss sprites and thus ensuring that the\n\tboss sprites end up at specific master.lib sprite IDs (\u003cq\u003epatnums\u003c/q\u003e). The\n\thardcoded boss rendering function then expects these sprites to be available\n\tat these exact IDs… which means that \u003ci\u003ethe otherwise hardcoded bosses can't\n\trender properly without the dialog script running before them\u003c/i\u003e.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThere is absolutely no excuse for the game to burden dialog scripts with\n\tthis functionality. Sure, delayed deallocation would allow them to blit\n\tstage-specific sprites, but the original games don't do that; probably\n\tbecause none of the two games feature an unblitting command. And \u003ci\u003eeven if\n\tthey did\u003c/i\u003e, it would have still been cleaner to expose the boss-specific\n\tsprite setup as a single script command that can then also be called from\n\tgame code if the script didn't do so. Commands like these just are a recipe\n\tfor crashes, \u003ci\u003eespecially\u003c/i\u003e with parsers that expect fullwidth Shift-JIS\n\ttext and where misaligned ASCII text can easily cause these commands to be\n\tskipped.\n\u003c/p\u003e\u003cp\u003e\n\tBut then again, it does make for funny screenshot material if you\n\taccidentally the deallocation and then see bosses being turned into stage\n\tenemies:\n\u003c/p\u003e\u003cfigure class=\"pixelated singleplayer_playfield\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH04-no-dialog-sprite-deallocation-1.png?fe797ced\"\n\t\tdata-title=\"Marisa\"\n\t\talt=\"TH04's dialog before the Stage 4 Marisa fight without deallocating the stage sprites inside the script, causing Marisa to be turned into one of the stage enemies\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH04-no-dialog-sprite-deallocation-2.png?1ef075ef\"\n\t\tdata-title=\"Yuuka\"\n\t\talt=\"TH04's dialog before the Stage 6 Yuuka fight without deallocating the stage sprites inside the script, causing Yuuka to be turned into two different cels of the same stage enemy\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH05-no-dialog-sprite-deallocation-1.png?13970946\"\n\t\tdata-title=\"Louise\"\n\t\talt=\"TH05's dialog before the Louise fight without deallocating the stage sprites inside the script, causing Louise to be turned into one of the ice enemies from TH05's Stage 2\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-11-01-TH05-no-dialog-sprite-deallocation-2.png?7dc8c6b5\"\n\t\tdata-title=\"Mai \u0026 Yuki\"\n\t\talt=\"TH05's dialog before the Louise fight without deallocating the stage sprites inside the script, causing Mai and Yuki to be turned into a windmill and fairy/demon enemy, respectively\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tSome of the more amusing consequences of not calling the\n\t\tsprite-deallocating\n\t\t\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u0026nbsp;\u003ccode\u003e\\c\u003c/code\u003e\u0026nbsp;/\u0026nbsp;\n\t\t\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u0026nbsp;\u003ccode\u003e0x04\u003c/code\u003e command inside a dialog\n\t\tscript.\u003cbr\u003e\n\t\tIn the case of 4️⃣, the game then even crashes on this frame at the end\n\t\tof the dialog, in a way that resembles the infamous\n\t\t\u003ca href=\"/blog/2021-11-29\"\u003e📝 TH04 crash before Stage 5 Yuuka if no EMS driver is loaded\u003c/a\u003e.\n\t\tBoth the stage- and boss-specific BFNT sprites are loaded into memory at\n\t\tthis point, leaving no room for the 256×256-pixel background image on\n\t\tthe size-limited master.lib heap.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp id=\"ref-2023-11-01\"\u003e\n\tWith all the general details out of the way, here's the command reference:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"vm comparison\"\u003e\n\t\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/th\u003e\n\t\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/th\u003e\n\t\t\u003cth\u003e\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr id=\"2023-11-01-speaker\"\u003e\n\t\t\u003ctd\u003e0\u003cbr\u003e1\u003c/td\u003e\n\t\t\u003ctd\u003e0x00\u003cbr\u003e0x01\u003c/td\u003e\n\t\t\u003ctd\u003eSelects either the player character (0) or the boss (1) as the\n\t\tcurrently speaking character, and moves the cursor to the beginning of\n\t\tthe text box. In TH04, this command also directly starts the new dialog\n\t\tbox, which is probably why it's not prefixed with a \u003ccode\u003e\\\u003c/code\u003e as it\n\t\tonly makes sense outside of text. TH05 requires a separate \u003ca\n\t\thref=\"#2023-11-01-th05-start\"\u003e\u003ccode\u003e0x0D\u003c/code\u003e command\u003c/a\u003e to do the\n\t\tsame.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-face\"\u003e\n\t\t\u003ctd\u003e\\=\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x02 \u003cvar\u003e0x!!\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eReplaces the face portrait of the currently active speaking\n\t\tcharacter with image #\u003cvar class=\"default\"\u003e1\u003c/var\u003e within her .CD2\n\t\tfile.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\=255\u003c/td\u003e\n\t\t\u003ctd\u003e0x02 0xFF\u003c/td\u003e\n\t\t\u003ctd\u003eRemoves the face portrait from the currently active text box.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-load\"\u003e\n\t\t\u003ctd\u003e\\l,\u003cvar\u003efilename\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x03 \u003cvar\u003efilename\u003c/var\u003e 0x00\u003c/td\u003e\n\t\t\u003ctd\u003eCalls master.lib's \u003ccode\u003esuper_entry_bfnt()\u003c/code\u003e function, which\n\t\tloads sprites from a BFNT file to consecutive IDs starting at the\n\t\tcurrent patnum write cursor.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\c\u003c/td\u003e\n\t\t\u003ctd\u003e0x04\u003c/td\u003e\n\t\t\u003ctd\u003eDeallocates all stage-specific BFNT sprites (i.e., stage enemies and\n\t\tmidbosses), freeing up conventional RAM for the boss sprites and\n\t\tensuring that master.lib's patnum write cursor ends up at\n\t\t\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u0026nbsp;128\u0026nbsp;/\n\t\t\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u0026nbsp;180.\u003cbr\u003e\n\t\tIn TH05's Extra Stage, this command also replaces\n\t\t\u003ca href=\"/blog/2023-06-30\"\u003e📝 the sprites loaded from \u003ccode\u003eMIKO16.BFT\u003c/code\u003e with the ones from \u003ccode\u003eST06_16.BFT\u003c/code\u003e\u003c/a\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\d\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eDeallocates all face portrait images.\u003cbr\u003e\n\t\tThe game automatically does this at the end of each dialog sequence.\n\t\tHowever, ZUN wanted to load Stage 6 Yuuka's 76\u0026nbsp;KiB of additional\n\t\tanimations inside the script via \u003ca\n\t\thref=\"#2023-11-01-load\"\u003e\u003ccode\u003e\\l\u003c/code\u003e\u003c/a\u003e, and would have once again\n\t\trun up against the master.lib heap size limit without that extra free\n\t\tmemory.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\m,\u003cvar\u003efilename\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x05 \u003cvar\u003efilename\u003c/var\u003e 0x00\u003c/td\u003e\n\t\t\u003ctd\u003eStops the currently playing BGM, loads a new one from the given\n\t\tfile, and starts playback.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd class=\"unused\"\u003e\\m$\u003c/td\u003e\n\t\t\u003ctd\u003e0x05 \u003cvar\u003e$\u003c/var\u003e 0x00\u003c/td\u003e\n\t\t\u003ctd\u003eStops the currently playing BGM.\u003cbr\u003e\n\t\tNote that TH05 interprets \u003cvar\u003e$\u003c/var\u003e as a null-terminated filename as\n\t\twell.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr class=\"unused\"\u003e\n\t\t\u003ctd\u003e\\m*\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eRestarts playback of the currently loaded BGM from the\n\t\tbeginning.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\b\u003cvar class=\"default\"\u003e0\u003c/var\u003e,\u003cvar class=\"default\"\u003e0\u003c/var\u003e,\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x06 \u003cvar\u003e0x!!!!\u003c/var\u003e \u003cvar\u003e0x!!!!\u003c/var\u003e \u003cvar\u003e0x!!\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eBlits the master.lib patnum with the ID indicated by the third\n\t\tparameter to the current VRAM page at the top-left screen position\n\t\tindicated by the first two parameters.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr class=\"unused\"\u003e\n\t\t\u003ctd\u003e\\e\u003cvar\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003ePlays the sound effect with the given ID.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr class=\"unused\"\u003e\n\t\t\u003ctd\u003e\\t\u003cvar class=\"default\"\u003e100\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eSets palette brightness via master.lib's\n\t\t\u003ccode\u003epalette_settone()\u003c/code\u003e to any value from 0 (fully black) to 200\n\t\t(fully white). 100 corresponds to the palette's original colors.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr class=\"unused\"\u003e\n\t\t\u003ctd\u003e\n\t\t\t\\fo\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003cbr\u003e\n\t\t\t\\fi\u003cvar class=\"default\"\u003e1\u003c/var\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eCalls master.lib's \u003ccode\u003epalette_black_\u003cb\u003eo\u003c/b\u003eut()\u003c/code\u003e or\n\t\t\u003ccode\u003epalette_black_\u003cb\u003ei\u003c/b\u003en()\u003c/code\u003e to play a hardware palette fade\n\t\tanimation from or to black, spending roughly \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e frame on each of the 16 fade steps.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\n\t\t\t\\wo\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003cbr\u003e\n\t\t\t\\wi\u003cvar class=\"default\"\u003e1\u003c/var\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t0x09 \u003cvar\u003e0x!!\u003c/var\u003e\u003cbr\u003e\n\t\t\t0x0A \u003cvar\u003e0x!!\u003c/var\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003eCalls master.lib's \u003ccode\u003epalette_white_\u003cb\u003eo\u003c/b\u003eut()\u003c/code\u003e or\n\t\t\u003ccode\u003epalette_white_\u003cb\u003ei\u003c/b\u003en()\u003c/code\u003e to play a hardware palette fade\n\t\tanimation from or to white, spending roughly \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e frame on each of the 16 fade steps.\u003cbr\u003e The\n\t\tTH05 version of \u003ccode\u003e0x09\u003c/code\u003e also clears the text in both boxes\n\t\tbefore the animation.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-linebreak\"\u003e\n\t\t\u003ctd\u003e\\n\u003c/td\u003e\n\t\t\u003ctd\u003e0x0B\u003c/td\u003e\n\t\t\u003ctd\u003eStarts a new line by resetting the X coordinate of the TRAM cursor\n\t\tto the left edge of the text area and incrementing the Y coordinate.\n\t\t\u003cbr\u003e\n\t\tThe new line will always be the next one below the last one that was\n\t\tproperly started, regardless of whether the text previously wrapped to\n\t\tthe next TRAM row at the edge of the screen.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\g\u003cvar class=\"default\"\u003e8\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003ePlays a blocking \u003cvar class=\"default\"\u003e8\u003c/var\u003e-frame screen shake\n\t\tanimation. Copy-pasted from the cutscene parser, but actually used right\n\t\tat the end of the dialog shown before TH04's Bad Ending.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-gaiji\"\u003e\n\t\t\u003ctd\u003e\\ga\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x0C \u003cvar\u003e0x!!\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e with the given ID from 0 to 255\n\t\tat the current cursor position, ignoring the per-glyph delay.\n\t\u003c/tr\u003e\u003ctr class=\"unused\"\u003e\n\t\t\u003ctd\u003e\\k\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eWaits \u003cvar class=\"default\"\u003e0\u003c/var\u003e frames (0 = forever) for any key\n\t\tto be pressed before continuing script execution.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-th05-start\"\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x0D\u003c/td\u003e\n\t\t\u003ctd\u003eStarts a new dialog box with the \u003ca\n\t\thref=\"#2023-11-01-speaker\"\u003epreviously selected speaker\u003c/a\u003e. All text\n\t\tuntil the next \u003ca href=\"#2023-11-01-th05-halt\"\u003e\u003ccode\u003e0xFF\u003c/code\u003e\n\t\tcommand\u003c/a\u003e will appear on screen.\u003cbr\u003e\n\t\t\u003cspan class=\"unused\"\u003eInside dialogs, this is a no-op.\u003c/span\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-th05-boxwipe\"\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0x0E\u003c/td\u003e\n\t\t\u003ctd\u003eTakes the current dialog cursor as the top-left corner of a\n\t\t240×48-pixel rectangle, and replaces all text RAM characters within that\n\t\trectangle with whitespace.\u003cbr\u003e\n\t\tThis is only used to clear the player character's text box before\n\t\tShinki's final \u003cspan lang=\"ja\"\u003e\u003cq\u003eいくよ‼\u003c/q\u003e\u003c/span\u003e box. Shinki has two\n\t\tconsecutive text boxes in all 4 scripts here, and ZUN probably wanted to\n\t\tclear the otherwise blue text to imply a dramatic pause before Shinki's\n\t\tfinal sentence. Nice touch.\u003cbr\u003e\n\t\t(You could, however, also use it after \u003ca href=\"#2023-11-01-th05-halt\"\u003ea\n\t\tbox-ending \u003ccode\u003e0xFF\u003c/code\u003e command\u003c/a\u003e to mess with text RAM in\n\t\tgeneral.)\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\#\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eQuits the currently running loop. This returns from either the text\n\t\tloop to the command loop, or it ends the dialog sequence by returning\n\t\tfrom the command loop back to gameplay. If this stage of the game later\n\t\tstarts another dialog sequence, it will start at the next script\n\t\tbyte.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\\$\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eLike \u003ccode\u003e\\#\u003c/code\u003e, but first waits for any key to be\n\t\tpressed.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2023-11-01-th05-halt\"\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e0xFF\u003c/td\u003e\n\t\t\u003ctd\u003eBehaves like TH04's \u003ccode\u003e\\$\u003c/code\u003e in the text loop, and like\n\t\t\u003ccode\u003e\\#\u003c/code\u003e in the command loop. Hence, it's not possible in TH05 to\n\t\tautomatically end a text box and advance to the next one without waiting\n\t\tfor a key press.\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tUnused commands are in \u003cspan class=\"unused\"\u003egray\u003c/span\u003e.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAt the end of the day, you might criticize the system for how its landmines\n\tmake it annoying to mod in ASCII text, but it all works and does what it's\n\tsupposed to. ZUN could have written the cleanest single and central\n\tShift-JIS iterator that properly chunks a byte buffer into halfwidth and\n\tfullwidth codepoints, and I'd still be throwing it out for the upcoming\n\tnon-ASCII translations in favor of something that either also supports UTF-8\n\tor performs dictionary lookups with a full box of text.\u003cbr\u003e\n\tThe only \u003ci\u003eactual\u003c/i\u003e bug can be found in the input detection, which once\n\tagain doesn't correctly handle \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/commit/8dfc2cd\"\u003ethe infamous \u003ci\u003ekey\n\tup\u003c/i\u003e/\u003ci\u003ekey down\u003c/i\u003e scancode quirk of PC-98 keyboards\u003c/a\u003e. All it takes\n\tis one wrongly placed input polling call, and suddenly you have to think\n\tabout how the update cycle behind the PC-98 keyboard state bytes\n\t\u003ci\u003emight\u003c/i\u003e cause the game to run the regular 2-frame delay for a single\n\t2-byte chunk of text before it shows the full text of a box after\n\tall… But even this bug is highly theoretical and could probably only be\n\tobserved very, \u003ci\u003every\u003c/i\u003e rarely, and exclusively on real hardware.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"th02-2023-11-01\"\u003e\n\tThe same can't be said about TH02 though, but more on that later. Let's\n\tfirst take a look at its data, which started out much simpler in that game.\n\tThe \u003ccode\u003eSTAGE?.TXT\u003c/code\u003e files contain just raw Shift-JIS text with no\n\ttrace of commands or structure. Turning on the whitespace display feature in\n\tyour editor reveals how the dialog system even assumes a fixed \u003ci\u003ebyte\u003c/i\u003e\n\tlength for each box: 36 bytes per line which will appear on screen, followed\n\tby 4 bytes of padding, which the original files conveniently use to visually\n\tsplit the lines via a CR/LF newline sequence. Make sure to disable trimming\n\tof trailing whitespace in your editor to not ruin the file when modding the\n\ttext… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre lang=\"ja\"\u003e靈夢：あんた、まだ名前も聞いてないの··\n······に覚えられないわよ。・・・・・··\n里香：あたいは、里香よ。覚えときなさ··\n・・・い。・・・・・・················\u003c/pre\u003e\u003cfigcaption\u003e\n\tTwo boxes from TH02's \u003ccode\u003eSTAGE5.TXT\u003c/code\u003e with visualized whitespace.\n\tThese also demonstrate how the CR/LF newlines only make up 2 of the 4\n\tpadding bytes, and require each line to be padded with two more bytes; you\n\tcould \u003ci\u003enot\u003c/i\u003e use these trailing spaces for actual text. Also note how\n\tthe exquisite mixture of fullwidth and halfwidth spaces demands the text to\n\tbe viewed with only the most metrically consistent monospace fonts to\n\tpreserve the intended alignment. 🍷 It appears quite misaligned on my phone.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tConsequently, everything else is hardcoded – every effect shown between text\n\tboxes, the face portrait shown for each box, and even how many boxes are\n\tpart of each dialog sequence. Which means that the source code now contains\n\t\u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/ae2fc2865a74b095bdbc8b469073f43c8cbc2d98/th02_main.asm#L29994-L30170\"\u003ea\n\tlong hardcoded list of face IDs for most of the text boxes in the game\u003c/a\u003e,\n\twith the rest being part of \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/ae2fc2865a74b095bdbc8b469073f43c8cbc2d98/th02/main/dialog/dialog.cpp#L429-L602\"\u003ethe\n\tdedicated hardcoded dialog scripts for \u003csup\u003e2\u003c/sup\u003e/\u003csub\u003e3\u003c/sub\u003e of the\n\tgame's stages.\u003c/a\u003e\u003cbr\u003e\n\tWithout the restriction to a fixed set of scripting commands, TH02 naturally\n\tgravitated to having the most varied dialog sequences of all PC-98 Touhou\n\tgames. This flexibility certainly facilitated Mima's grand entrance\n\tanimation in Stage 4, or the different lines in Stage 4 and 5 depending on\n\twhether you already used a continue or not. Marisa's post-boss dialog even\n\tinserts the number of continues into the text itself – by, you guessed it,\n\twriting to hardcoded byte offsets inside the dialog text before printing it\n\tto the screen. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e But once again, I have nothing to\n\tcriticize here – not even the fact that the alternate dialog scripts have to\n\tmutate the \"box cursor\" to jump to the intended boxes within the file. I\n\tknow that some people in my audience like VMs, but I would have considered\n\tit \u003ci\u003emore\u003c/i\u003e bloated if ZUN had implemented a full-blown scripting\n\tlanguage just to handle all these special cases.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"th02-face-2023-11-01\"\u003e\n\tAnother unique aspect of TH02 is the way it stores its face portraits, which\n\tare infamous for how hard they are to find in the original data files. These\n\tsprites are actually \u003ci\u003emap tiles\u003c/i\u003e, stored in \u003ccode\u003eMIKO_K.MPN\u003c/code\u003e,\n\tand drawn using the same functions used to blit the regular map tiles to the\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 tile source area in VRAM\u003c/a\u003e. We can only guess\n\twhy ZUN chose this one out of the three graphics formats he used in TH02:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eBFNT supports transparency, but sacrifices one of the 16 colors to do\n\tso. ZUN only used 15 colors for the face portraits, but might have wanted to\n\tkeep open the option to use that 16\u003csup\u003eth\u003c/sup\u003e color. The detailed\n\tbackgrounds also suggest that these images were never supposed to be\n\ttransparent to begin with. \u003c/li\u003e\n\t\u003cli\u003ePI is used for all bigger and non-transparent images, but ZUN would have\n\thad to write a separate small function to blit a 48×48 subsection of such an\n\timage. That certainly wouldn't have stopped him in the TH01 days, but he\n\tprobably was already past that point by this game.\u003c/li\u003e\n\t\u003cli\u003eThat only leaves .MPN. Sure, he did have to slice each face into 9\n\tseparate 16×16 \"map\" tiles to use this format, but that's a small price to\n\tpay in exchange for not having to write any new low-level blitting code,\n\tespecially since he must have already had an asset pipeline to generate\n\tthese files.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure class=\"pixelated\" id=\"facetiles-2023-11-01\"\u003e\n\t\u003cfigcaption\u003e\n\t\t\u003cform\u003e\n\t\t\t\u003cinput type=\"checkbox\" id=\"grid-toggle-2023-11-01\" checked onchange=\"\n\t\t\t\tconst tiles = document.getElementById('miko_k-2023-11-01');\n\t\t\t\tconst grid = document.getElementById('miko_k-grid-2023-11-01');\n\t\t\t\tif(this.checked) {\n\t\t\t\t\tgrid.hidden = false;\n\t\t\t\t\ttiles.hidden = true;\n\t\t\t\t} else {\n\t\t\t\t\ttiles.hidden = false;\n\t\t\t\t\tgrid.hidden = true;\n\t\t\t\t}\n\t\t\t\"\u003e\n\t\t\t\u003clabel for=\"grid-toggle-2023-11-01\"\u003eShow tile ID grid\u003c/label\u003e\n\t\t\u003c/form\u003e\n\t\tTH02's \u003ccode\u003eMIKO_K.PTN\u003c/code\u003e, arranged into a 16×16-tile layout that\n\t\treveals how these tiles are combined into face portraits.\u003cbr\u003e\n\t\t\u003ccode\u003eMPNDEF\u003c/code\u003e from \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e's \u003ca\n\t\thref=\"http://lunarcast.net/mystictk.php\"\u003eMysticTK\u003c/a\u003e conveniently uses\n\t\tthis exact layout in its .BMP output. Earlier \u003ccode\u003eMPNDEF\u003c/code\u003e\n\t\tversions crashed when converting this file as its 256 tiles led to an\n\t\t8-bit overflow bug, so make sure you've updated to the current version\n\t\tfrom the end of October 2023 if you want to convert this file yourself.\n\t\tThe format stores the 4 bitplanes of each 16×16 tile in order, so good\n\t\tluck finding a different planar image viewer that would support both\n\t\tsuch a tiled layout \u003ci\u003eand\u003c/i\u003e a custom palette. Sometimes, a weird\n\t\tinternal format is the best type of obfuscation. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/figcaption\u003e\n\t\u003cdiv class=\"multilayer\" style=\"aspect-ratio: 1 / 1;\"\u003e\n\t\t\u003cimg\n\t\t\tid=\"miko_k-2023-11-01\"\n\t\t\tsrc=\"/blog/static/2023-11-01-TH02-MIKO_K.PTN.png?c6338187\"\n\t\t\tstyle=\"width: 512px; left: unset; top: unset; right: 0; bottom: 0;\"\n\t\t\talt=\"TH02's MIKO_K.PTN\"\n\t\t\thidden\n\t\t\u003e\n\t\t\u003cimg\n\t\t\tid=\"miko_k-grid-2023-11-01\"\n\t\t\tsrc=\"/blog/static/2023-11-01-TH02-MIKO_K.PTN-with-grid.png?1ee4ec8f\"\n\t\t\talt=\"TH02's MIKO_K.PTN with the 16×16 tile grid overlaid\"\n\t\t\u003e\n\t\u003c/div\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd since you're certainly wondering about all these black tiles at the\n\tedges: Yes, these are not only part of the file and pad it from the required\n\t240×192 pixels to 256×256, but also kept in memory during a stage, wasting\n\t9.5\u0026nbsp;KiB of conventional RAM. That's 172 seconds of potential input\n\treplay data, just for those people who might still think that we need EMS\n\tfor replays.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"th02-start-2023-11-01\"\u003e\n\tAlright, we've got the text, we've got the faces, let's slide in the box and\n\tdisplay it all on screen. Apparently though, we also have to blit the player\n\tand option sprites using raw, low-level master.lib function calls in the\n\tprocess? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e This can't be right, especially because ZUN\n\talways blits the option sprite associated with the Reimu-A shot type,\n\tregardless of which one the player actually selected. And if you keep moving\n\tabove the box area before the dialog starts, you get to see exactly how\n\twrong this is:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Dialog-start-bugs.webp?be8b98fd\" preload=\"none\" controls width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"60\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-start-bugs.avi?49eb7e29\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Dialog-start-bugs.webm?fd5d62f2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Dialog-start-bugs.webm?17830513\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Dialog-start-bugs.webm?f72cf100\" type=\"video/webm\"\u003eVideo demonstrating all 4 bugs during TH02's dialog box slide-in animation. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-start-bugs.avi?49eb7e29\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"37\" data-title=\"Box slides in\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tLet's look closer at Reimu's sprite during the slide-in animation, and in\n\tthe two frames before:\n\u003c/p\u003e\u003cfigure class=\"pixelated\" id=\"bugs-2023-11-01\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABACAMAAAC6GQAEAAAAJFBMVEWadXUAAABVAABlVSCaVUWqAAD/AAD///+6VbqKAIpVihC6ut8RUTigAAABwElEQVR42u2X0W7CMAxF76lrl5X//9+RmGwVSwVb8jaO1SQoV0ehEhYRk9ED6/Dn62Sk6+R68+afAvoNqz0pAFtfLi1PALaNwvIS4gVSOqcf0ubKhH7I1wgb4/0QcqwzjPdDcmQnZ41CeiAXw8bmMWzOEdMyXZhM+xXDRwHG3yFA9V0ul2LEjFEfxXeYRoVlSFHOGlKCtu2hQwjx537YF2L8tR8CPAiBhV/2w4DgKed5HQlp/V5ywnrMN+KsH8Z2I+oSA3cKHrcCBxCdfL8fhsp+oayumLuHp68+qb/SzXf7Yd3PhFSEEeFFBkTOiG6+R+i+e0PRhEXpQPFFnrCf7xBbxG0/E1WYPir5/RH9fAevgVJbuN+FfoPEg4Lo5js+RZ6/BuQYXqHh6TvJd4Sep/dwL0JI3xED0c33j+iHfRphNMwQJ/kO7mU3Efq22EGIOMl38B8+2w1LI3sdxUn+hGYTlT0tmGH7Thr7+dN+SCtWyNNZQmGhnz/th9xroWK27+UpIxhwlk/EU5qw2sfvy2ApBDAm3JdpL5L8mzh+X6aiVhPuywBStU0C0GxjTtJ/MgpmG1PG+zb9g9XmlpbJfAKYWSRpEPtACAAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Frame 35\"\n\t\talt=\"Zoomed-in area around Reimu's sprite from frame 35 of the video above\"\n\t\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABACAMAAAC6GQAEAAAAJFBMVEWadXX///+6ut+6VbpVihCaVUVlVSD/AACKAIqqAABVAAAAAACuhtTuAAAB2klEQVR42u2X0a4aMQwFT7i7dr3z///bTaxcIWqKyqaVKjGGZCOORuYBi4jF6IH98vm2GOm2uD78f8BqHyz2tcZCK4KUrvIl/EmTuz0vvldoFImidDwHcoXvw2vEa7ZV85B+ZqaAi/MQcjWMfLo4D+krKRxnLs5DxpIs+xXDjw5IV7UAw/f19dWNmHHVR/fdbVeFEqQod11SglrjntaEWCy8YAR4EPKGz/trQI2o87Xs/hFq6vyzeejtxMczBhF0ws+CANgp8vU89PPVBt4PWER4pG+8U39Q5st56K0lDnShu0eXAZ57UuYnmrjawE/kSuFQBtB9zuDYi3w1D725t+YjcbtNYQSD/P4YSEW+mIcxAr2aRyiFcUISnv2hW5H/dR6GPPsfAQVGDJgEgEC3Kq9HQpHdh0d0IaTvHiM7LPIqjB53nzNxY2KWHVb5ggg/KxGCabE7IQd1vjY++mwzLI1sYxXUedVMmxhsacEM2zZAHNR5PYNZCLI7S+gIqvxrnxiYbVt/9xUsOyzzrwEETOGwww66AlgKAQxpP9jf/H+YBdjJnIQcB4e9+f8wYXDM6sLfo39/XyZDiFX3Zcht3X0Zxs66+zLkpnXAamPK+NymP/wddltbOhbzE0ExQQX1ARnkAAAAAElFTkSuQmCC\"\n\t\tdata-title=\"Frame 36\"\n\t\talt=\"Zoomed-in area around Reimu's sprite from frame 36 of the video above\"\n\t\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABABAMAAAB/6e0FAAAAJFBMVEWadXX///+6ut+6VbpVihCaVUVlVSD/AACKAIqqAABVAAAAAACuhtTuAAACI0lEQVR42t2VMWvjMBiGRbhSwrecsmS4JfFyQ8b7C6FLdy9dgxYtzWTcG28JeOlmjH9AIVl0oYg4z587JZfaLi216NgHkY+8PE4MepEUkagLV4MziUSNIlFfGXGRIsSZAnGqCDrKdCKgUe+QvsL71KdA+ga17bM7Hncct/zYvkF9ro8iV/DsBDfQRxYJsE9goI97BKwdwUAfnTuLSjG00Y4CEAY3mgObTeGtMLTRxeHPr83QhsszsIFDAfCR6FEUQIFw+6HoNGf+fsfGirzvXKv7IAbjIoJzbd7XTtwLlhcM0ub9Pq71NE3voIGyrGnw3rd518eHJ631w9MOyrrJmzwM2LV5r4/rEEyBJsub3+RhYLu86+N9+J5l1+KbLKtr6jLLsHdt3vXx53Q90TpZnMQczp923+ZdH8vpeq71RGjKvAaqModFm7d9HF/riQ4soKnqgqKqapxr85m6MB6v1+HBZAFU9WFDVWFZtHkrqsd8nuh5kmBoKopge+Ncl7fi+HGeTCZ6JoYL3iJdrjpzrpNEzxyAxVjAuS5XHaPwJjPlYGWNMfbGeqTL++IoGSn5ZlhawC8xuDZ/xfm58EvmP36FdLmo1zi/tH51Y1bhn5fOdbl7c4iu4DmIt3BLL5f++ehPkOKDaD0cTS/vn4+7wGngjQV2x37++fMxTAFAIu5rSNjvI+5rIayo+5rTikGC5mJElHMSIzoViTutL04aidpG8g9rsB8etXIeNQAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Frame 37\"\n\t\talt=\"Zoomed-in area around Reimu's sprite from frame 37 of the video above\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis one image shows off no less than \u003ci\u003e4\u003c/i\u003e bugs:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eZUN blits the stationary player sprite here, regardless of whether the\n\tplayer was previously moving left or right. This is a nice way of indicating\n\tthat Reimu stops moving once the dialog starts, but \u003ci\u003emaybe\u003c/i\u003e ZUN should\n\thave unblitted the old sprite so that the new one wouldn't have appeared on\n\ttop. The game only unblits the 384×64 pixels covered by the dialog box on\n\tevery frame of the slide-in animation, so Reimu would only appear correctly\n\tif her sprite happened to be entirely located within that area.\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eAll sprites are shifted up by 1 pixel in frame 2️⃣. This one is not a\n\tbug in the dialog system, but in the main game loop. The game runs the\n\trelevant actions in the following order:\u003c/p\u003e\u003col\u003e\n\t\t\u003cli\u003eInvalidate any map tiles covered by entities\u003c/li\u003e\n\t\t\u003cli\u003eRedraw invalidated tiles\u003c/li\u003e\n\t\t\u003cli\u003eDecrement the Y coordinate at the top of VRAM according to the\n\t\tscroll speed\u003c/li\u003e\n\t\t\u003cli\u003eUpdate and render all game entities\u003c/li\u003e\n\t\t\u003cli\u003eScroll in new tiles as necessary according to the scroll speed, and\n\t\treport whether the game has scrolled one pixel past the end of the\n\t\tmap\u003c/li\u003e\n\t\t\u003cli\u003eIf that happened, pretend it didn't by incrementing the value\n\t\tcalculated in #3 for all further frames and skipping to\n\t\t\u003ccode\u003e#8\u003c/code\u003e.\u003c/li\u003e\n\t\t\u003cli\u003eIssue a GDC \u003ccode\u003eSCROLL\u003c/code\u003e command to reflect the line\n\t\tcalculated in #3 on the display\u003c/li\u003e\n\t\t\u003cli\u003eWait for VSync\u003c/li\u003e\n\t\t\u003cli\u003eFlip VRAM pages\u003c/li\u003e\n\t\t\u003cli\u003eStart boss if we're past the end of the map\u003c/li\u003e\n\t\u003c/ol\u003e\u003cp\u003e\n\t\tThe problem here: Once the dialog starts, the game has already rendered\n\t\tan entire new frame, with all sprites being offset by a new Y scroll\n\t\toffset, without adjusting the graphics GDC's scroll registers to\n\t\tcompensate. Hence, the Y position in 3️⃣ is the correct one, and the\n\t\twhole existence of frame 2️⃣ is a bug in itself. (Well… OK, probably a\n\t\tquirk because speedrunning exists, and it would be pretty annoying to\n\t\tsynchronize any video regression tests of the future TH02 Anniversary\n\t\tEdition if it renders one fewer frame in the middle of a stage.)\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eZUN blits the option sprites to their position from frame 1️⃣. This\n\tbrings us back to\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 TH02's special way of retaining the previous and current position in a two-element array, indexed with a VRAM page ID\u003c/a\u003e.\n\tNormally, this would be equivalent to using dedicated \u003ccode\u003eprev\u003c/code\u003e and\n\t\u003ccode\u003ecur\u003c/code\u003e structure fields and you'd just index it with the back page\n\tfor every rendering call. But if you then decide to go single-buffered for\n\tdialogs and render them onto the \u003ci\u003efront\u003c/i\u003e page instead…\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tNote that fixing bug #2 would not cancel out this one – the sprites would\n\tthen simply be rendered to their position in the frame before 1️⃣.\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eAnd of course, the fixed option sprite ID also counts as a bug.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tAs for the boxes themselves, it's yet another loop that prints 2-byte chunks\n\tof Shift-JIS text at an even slower fixed interval of 3 frames. In an\n\tinteresting quirk though, ZUN assumes that every box starts with the name of\n\tthe speaking character in its first two fullwidth Shift-JIS characters,\n\tfollowed by a fullwidth colon. These 6 bytes are displayed immediately at\n\tthe start of every box, without the usual delay. The resulting alignment\n\tlooks rather janky with Genjii, whose single right-padded \u003cspan lang=\"ja\"\u003e亀\n\t\u003c/span\u003e kanji looks quite awkward with the fullwidth space between the name\n\tand the colon. Kind of makes you wonder why ZUN just didn't spell out his\n\tproper name, \u003cspan lang=\"ja\"\u003e玄爺\u003c/span\u003e, instead, but I get the stylistic\n\tdifference.\u003cbr\u003e\n\tIn Stage 4, the two-kanji assumption then breaks with Marisa's three-kanji\n\tname, which causes the full-width colon to be printed as the first delayed\n\tcharacter in each of her boxes:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Dialog-1-kanji-name.webp?bcc26ea3\" preload=\"none\" controls data-title=\"One-kanji name\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"118\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-1-kanji-name.avi?a163f0b6\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Dialog-1-kanji-name.webm?d3c05375\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Dialog-1-kanji-name.webm?7f05cbd5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Dialog-1-kanji-name.webm?6d795e0d\" type=\"video/webm\"\u003eVideo of a TH02 in-game dialog text box spoken by Genjii, using a prefix with the intended width of three full-width Shift-JIS characters. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-1-kanji-name.avi?a163f0b6\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Dialog-2-kanji-name.webp?873bb370\" preload=\"none\" controls data-title=\"Two-kanji name\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"118\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-2-kanji-name.avi?d391b04b\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Dialog-2-kanji-name.webm?f591af65\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Dialog-2-kanji-name.webm?b378f33b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Dialog-2-kanji-name.webm?d884ad49\" type=\"video/webm\"\u003eVideo of a TH02 in-game dialog text box whose text uses a prefix with the intended width of three full-width Shift-JIS characters. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-2-kanji-name.avi?d391b04b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Dialog-3-kanji-name.webp?566ae784\" preload=\"none\" controls data-title=\"Three-kanji name\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"118\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-3-kanji-name.avi?13166c49\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Dialog-3-kanji-name.webm?41d9ab57\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Dialog-3-kanji-name.webm?d7cef500\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Dialog-3-kanji-name.webm?ffd2c87e\" type=\"video/webm\"\u003eVideo of a TH02 in-game dialog text box spoken by Marisa, whose prefix exceeds the intended length of three full-width Shift-JIS characters by one character that is subsequently printed separately. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Dialog-3-kanji-name.avi?13166c49\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp id=\"th02-mima-2023-11-01\"\u003e\n\tThat's all the issues and quirks in the system itself. The scripts\n\tthemselves don't leave much room for bugs as they basically just loop over\n\tthe hardcoded face ID array at this level… until we reach the end of the\n\tgame. Previously, the slide-in animation could simply use the tile\n\tinvalidation and re-rendering system to unblit the box on each frame, which\n\talso explained why Reimu had to be separately rendered on top. But this no\n\tlonger works with a custom-rendered boss background, and so the game just\n\tchooses to flood-fill the area with graphics chip color #0:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Mima-form-change-dialog-start.webp?a9c3f30e\" preload=\"none\" controls width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"80\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-form-change-dialog-start.avi?7c0fc02d\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Mima-form-change-dialog-start.webm?ffb03e04\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Mima-form-change-dialog-start.webm?4e786475\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Mima-form-change-dialog-start.webm?4f8c39d1\" type=\"video/webm\"\u003eVideo of the box slide-in animation during the form change dialog of the TH02 Mima fight, demonstrating how the lack of proper unblitting caused the dialog system to just flood-fill the area on every frame of the animation. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-form-change-dialog-start.avi?7c0fc02d\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"57\" data-title=\"Box slides in\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThen again, transferring pixels from the back page would be just\n\tas wrong as they lag one frame behind. No way around capturing these 384×64\n\tpixels to main memory here… Oh well, this flood-fill at least adds even more\n\tlegibility on top of the already half-transparent text box. A property that\n\tthe following dialog sequence unfortunately lacks…\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFor Mima's final defeat dialog though, ZUN chose to not even show the box.\n\tHe might have realized the issue by that point, or simply preferred the more\n\tdramatic effect this had on the lines. The resulting issues, however, might\n\teven have ramifications for such un-technical things as \u003ci\u003elore\u003c/i\u003e and\n\t\u003ci\u003echaracter dynamics\u003c/i\u003e. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e As it turns out, the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/ae2fc2865a74b095bdbc8b469073f43c8cbc2d98/th02/main/dialog/dialog.cpp#L573-L586\"\u003ecode\n\tfor this dialog sequence\u003c/a\u003e does in fact render Mima's smiling face for all\n\tboxes?! You only don't see it in the original game because it's rendered to\n\tthe other VRAM page that remains invisible during the dialog sequence:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Mima-defeat-dialog-back.webp?a0ae01ea\" preload=\"none\" controls data-title=\"Back page\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"654\" style=\"aspect-ratio: 384 / 368\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-defeat-dialog-back.avi?e8157954\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Mima-defeat-dialog-back.webm?c82b8ae5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Mima-defeat-dialog-back.webm?9f248c72\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Mima-defeat-dialog-back.webm?2008e339\" type=\"video/webm\"\u003eVideo of the accessed/invisible VRAM page during the defeat dialog at the end of the TH02 Mima fight, showing the otherwise inivisible smiling portraits associated with each line. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-defeat-dialog-back.avi?e8157954\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-11-01-TH02-Mima-defeat-dialog-front.webp?14a4736d\" preload=\"none\" controls data-title=\"Front page / original game\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"654\" style=\"aspect-ratio: 384 / 368\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-defeat-dialog-front.avi?7a4d6e28\"\u003e\u003csource src=\"/blog/static/video/av1/2023-11-01-TH02-Mima-defeat-dialog-front.webm?fcd79533\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-11-01-TH02-Mima-defeat-dialog-front.webm?5ee7e6ae\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-11-01-TH02-Mima-defeat-dialog-front.webm?aa60af67\" type=\"video/webm\"\u003eVideo of the defeat dialog at the end of the TH02 Mima fight, in its original version that types white text on top of whatever was in VRAM during the previous frame. \u003ca href=\"/blog/static/video/zmbv/2023-11-01-TH02-Mima-defeat-dialog-front.avi?7a4d6e28\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eCaution, flashing lights.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHere's how I interpret the situation:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\tThe function that launches into the final part of the dialog script\n\t\tstarts with \u003ca\n\t\thref=\"https://github.com/nmlgc/ReC98/blob/ae2fc2865a74b095bdbc8b469073f43c8cbc2d98/th02_main.asm#L24004-L24056\"\u003ededicated\n\t\tcode to re-render Mima to the back page\u003c/a\u003e, on top of the previously\n\t\trendered planet background. Since the entire script runs on the front\n\t\tpage (and thus, on top of the previous frame) and the game launches into\n\t\tthe ending immediately after, you don't ever get to see this new partial\n\t\tframe in the original game.\u003cbr\u003e\n\t\tShowing this partial frame would also ensure that you can actually\n\t\t\u003ci\u003eread\u003c/i\u003e the dialog text without a surrounding box. Then, the white\n\t\tletters won't ever be put on top of any white bullets – or, worse, \u003ca\n\t\thref=\"https://youtu.be/bYxWi3WcagI\u0026t=210\"\u003ebe completely invisible if the\n\t\tdialog is triggered in the middle of Reimu-B's bomb animation, which\n\t\tfills VRAM with lots of white pixels\u003c/a\u003e. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\t\t\u003cbr\u003e\n\t\tHence, we've got enough evidence to classify not showing the back page\n\t\tas a \u003ca\n\t\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-bug\"\u003eZUN\n\t\tbug\u003c/a\u003e. 🐞\n\t\u003c/li\u003e\n\t\u003cli\u003e\n\t\tHowever, Mima's smiling face jars with the words she says here. Adding\n\t\tthe face would deviate more significantly from the original game than\n\t\tremoving the player shot, item, bullet, or spark sprites would. It's\n\t\timaginable that ZUN just forgot about the dedicated code that\n\t\tre-rendered just Mima to the back page, but the faces \u003ci\u003eadd\u003c/i\u003e\n\t\tsomething to the dialog, and ZUN would have \u003ci\u003eclearly\u003c/i\u003e noticed and\n\t\tfixed it if their absence wasn't intended. Heck, ZUN might have just put\n\t\tsomething related to Mima into the code because TH02's dialog system has\n\t\tno way of \u003ci\u003enot\u003c/i\u003e drawing a face for a dialog box. Filling the face\n\t\tarea with graphics chip color #0, as seen in the first and third boxes\n\t\tof the Extra Stage pre-boss dialog, would have been an alternative, but\n\t\tthat would have been equally wrong with regard to the background.\u003cbr\u003e\n\t\tHence, the invisible face portrait from the original game is a \u003ca\n\t\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-quirk\"\u003eZUN\n\t\tquirk\u003c/a\u003e. 🎺\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo, the future TH02 Anniversary Edition will fix the \u003ci\u003ebug\u003c/i\u003e by showing\n\tthe back page, but retain the \u003ci\u003equirk\u003c/i\u003e by rewriting the dialog code to\n\tnot blit the face.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"final-2023-11-01\"\u003e\n\tAnd with that, we've secured all in-game dialog for the upcoming non-ASCII\n\ttranslations! The remaining \u003csup\u003e2\u003c/sup\u003e/\u003csub\u003e3\u003c/sub\u003e of the last push made\n\tfor a good occasion to also decompile the small amount of code related to\n\tTH03's win messages, stored in the \u003ccode\u003e@0?TX.TXT\u003c/code\u003e files. Similar to\n\tTH02's dialog format, these files are also split into fixed-size blocks of\n\t3×60 bytes. But this time, TH03 loads all 60 bytes of a line, including the\n\tCR/LF line breaking codepoints in the original files, into the statically\n\tallocated buffer that it renders from. These control characters are then\n\tonly filtered to whitespace by ZUN's \u003ccode\u003egraph_putsa_fx()\u003c/code\u003e function.\n\tIf you remove the line breaks, you get to use the full 60 bytes on every\n\tline.\u003cbr\u003e\n\tThe final commits went to the \u003ccode\u003eMIKO.CFG\u003c/code\u003e loading and saving\n\tfunctions used in TH04's and TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e, as well as TH04's\n\tgame startup code to finally catch up with\n\t\u003ca href=\"/blog/2020-09-21\"\u003e📝 TH05's counterpart from over 3 years ago\u003c/a\u003e.\n\tThis brought us right in front of the main menu rendering code in both TH04\n\tand TH05, which is identical in both games and will be tackled in the next\n\tPC-98 Touhou delivery.\n\u003c/p\u003e\u003cp\u003e\n\tNext up, though: Returning to Shuusou Gyoku, and adding support for SC-88Pro\n\trecordings as BGM. Which may or may not come with a slight controversy…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-09-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-11-01T23:58:05Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-09-30",
      "url": "https://rec98.nmlgc.net/blog/2023-09-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-11-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-08-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-09-30\"\u003e\u003ctime datetime=\"2023-09-30T21:01:36Z\"\u003e2023-09-30 21:01\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0252\"\u003eP0252\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Building SDL from source within Tup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0251...e98feef\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0253\"\u003eP0253\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Game logic portability, part 1/? + Cross-platform APIs, part 1/?: Window creation via SDL)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/e98feef...24df71c\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0254\"\u003eP0254\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Cross-platform APIs, part 2/?: Sound via miniaudio)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/24df71c...b7b863b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0255\"\u003eP0255\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Cross-platform APIs, part 3/?: Input via SDL)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/b7b863b...2b8218e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0256\"\u003eP0256\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Restoring the screenshot feature + Translating inline ASM code to C++)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/2b8218e...P0256\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0257\"\u003eP0257\u003c/a\u003e\n\t\t\tWebsite (Video player code cleanup + audio support)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/b79c667...e2ba49b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e, Ember2528, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/input\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Processing data entered from the keyboard or a joypad.\"\u003einput\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\u003cstyle\u003e\n\t#waveform-2023-09-30 {\n\t\toverflow: scroll hidden;\n\t\twidth: 100%;\n\t}\n\t#waveform-2023-09-30 th {\n\t\tposition: relative;\n\t\tfont-family: monospace;\n\t\tpadding-left: 1ch;\n\t\tpadding-right: 1ch;\n\t\tbox-sizing: border-box;\n\t}\n\t#waveform-2023-09-30 th:after {\n\t\tcontent: attr(data-name);\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tz-index: 1;\n\t\ttext-shadow:\n\t\t\t-1px -1px 0 white,\n\t\t\t 1px -1px 0 white,\n\t\t\t-1px  1px 0 white,\n\t\t\t 1px  1px 0 white;\n\t}\n\t#waveform-2023-09-30 tbody td {\n\t\tpadding: 0;\n\t\tposition: relative;\n\t\theight: 151px;\n\t}\n\t#waveform-2023-09-30 tbody {\n\t\tborder-bottom: var(--table-border);\n\t}\n\t#waveform-2023-09-30 tbody td::after {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 50%;\n\t\twidth: 100%;\n\t\theight: 1px;\n\t\tbackground-color: black;\n\t\tcontent: \"\";\n\t}\n\t#waveform-2023-09-30 th:not(:last-child),\n\t#waveform-2023-09-30 td:not(:last-child) {\n\t\tborder-right: var(--table-border);\n\t}\n\t#waveform-2023-09-30 td img {\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\ttransform: translate(-50%, -50%);\n\t}\n\t#waveform-2023-09-30.clipped td img {\n\t\tclip-path: inset(73px 0px 73px 0px);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tAnd now we're taking this small indie game from the year 2000 and porting\n\tits game window, input, and sound to the industry-standard cross-platform\n\tAPI with \"simple\" in its name.\n\u003c/p\u003e\u003cp\u003e\n\tWhy did this have to be so complicated?! I expected this to take maybe 1-2\n\tweeks and result in an equally short blog post. Instead, it raised so many\n\tquestions that I ended up with the longest blog post so far, by quite a wide\n\tmargin. These pushes ended up covering so many aspects that could be\n\tinteresting to a general and non-Seihou-adjacent audience, so I think we\n\tneed a table of contents for this one:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#zig-2023-09-30\"\u003eEvaluating Zig\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#concepts-2023-09-30\"\u003eVisual Studio doesn't implement concepts correctly?\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#tup-2023-09-30\"\u003eReusable building blocks for Tup\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl-2023-09-30\"\u003eCompiling SDL\u0026nbsp;2\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#fps-2023-09-30\"\u003eThe new frame rate limiter\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdlaudio-2023-09-30\"\u003eAudio via SDL or SDL_mixer? (Nope, neither)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#miniaudio-2023-09-30\"\u003eminiaudio\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#resampling-2023-09-30\"\u003eResampling defective sound effects (including FLAC not always being lossless)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdlinput-2023-09-30\"\u003eJoypad input with SDL\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#screenshots-2023-09-30\"\u003eRestoring the original screenshot feature\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#utmath-2023-09-30\"\u003eInteger math in hand-written ASM\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr\u003e\u003cp id=\"zig-2023-09-30\"\u003e\n\tBefore we can start migrating to SDL, we of course have to integrate it into\n\tthe build somehow. On Linux, we'd ideally like to just dynamically link to a\n\tdistribution's SDL development package, but since there's no such thing on\n\tWindows, we'd like to compile SDL from source there. This allows us to reuse\n\tour debug and release flags and ensures that we get debug information,\n\twithout \u003ca href=\"https://vcpkg.io/\"\u003eneeding to clone build scripts for every\n\tC++ library ever in the process\u003c/a\u003e or something.\u003cbr\u003e\n\tSo let's get my Tup build scripts ready for compiling vendored libraries… or\n\tmaybe not? Recently, I've kept hearing about a \u003ca\n\thref=\"https://www.youtube.com/watch?v=YXrb-DqsBNU\u0026t=455s\"\u003ehot new\n\ttechnology\u003c/a\u003e that not only provides the rare kind of jank-free\n\tcross-compiling build system for C/C++ code, but innovates by even\n\t\u003ci\u003ebundling a C++ compiler\u003c/i\u003e into a single 279\u0026nbsp;MiB package with no\n\tfurther dependencies. Realistically replacing both Visual Studio and Tup\n\twith a single tool that could target every OS is quite a selling point. The\n\tupcoming Linux port makes for the perfect occasion to evaluate Zig, and to\n\tfind out whether Tup is still my favorite build system in 2023.\n\u003c/p\u003e\u003cp\u003e\n\tEven apart from its main selling point, there's a lot to like about Zig:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFirst and foremost: It's a modern systems programming language with\n\tseamless C interop that we could gradually migrate parts of the codebase to.\n\tThe feature set of the core language seems to hit the sweet spot between C\n\tand C++, although I'd have to use it more to be completely sure.\u003c/li\u003e\n\t\u003cli\u003eA native, optimized Hello World binary with no string formatting is\n\t4\u0026nbsp;KiB when compiled for Windows, and 6.4\u0026nbsp;KiB when cross-compiled\n\tfrom Windows to Linux. It's so refreshing to see a systems language in 2023\n\tthat doesn't bundle a bulky runtime for trivial programs and then defends it\n\twith the old excuse of \u003ci\u003e\"but all this runtime code will come in handy the\n\tlarger your program gets\"\u003c/i\u003e. With a first impression like this, Zig\n\tmanaged to realize the \"don't pay for what you don't use\" mantra that C++\n\ttypically claims for itself, but only pulls off maybe half of the time.\u003c/li\u003e\n\t\u003cli\u003eYou can \u003ca\n\thref=\"https://github.com/ziglang/zig/blob/ff61c428793ff382c8d521638b416ca288e53de5/lib/std/target/x86.zig\"\u003edirectly\n\ttarget specific CPU models, down to even the oldest 386 CPUs\u003c/a\u003e?! How\n\tamazing is that?! In contrast, Visual Studio only describes its \u003ccode\u003e\u003ca\n\thref=\"https://learn.microsoft.com/en-us/cpp/build/reference/arch-x86\"\u003e/arch:IA32\u003c/a\u003e\u003c/code\u003e\n\tcompatibility option in very vague terms, leaving it up to you to figure out\n\tthat \u003ci\u003e\"legacy 32-bit x86 instruction set without any vector\n\toperations\"\u003c/i\u003e actually means \u003ci\u003e\"i586/P5 Pentium, because the startup code\n\tstill includes an unconditional \u003ccode\u003eCPUID\u003c/code\u003e instruction\"\u003c/i\u003e. In any\n\tcase, it means that Zig could also cover the i586 build.\u003cul\u003e\n\t\t\u003cli\u003eEven better, changing Zig's CPU model setting recompiles both its\n\t\tbundled C/C++ standard library and Zig's own compiler-rt polyfill\n\t\tlibrary for that architecture. This ensures that no unsupported\n\t\tinstructions ever show up in the binary, and also removes the need for\n\t\tany \u003ccode\u003eCPUID\u003c/code\u003e checks. This is so much better than the Visual\n\t\tStudio model of linking against a fixed pre-compiled standard library\n\t\tbecause you don't have to trust that all these newer instructions\n\t\twouldn't actually be executed on older CPUs that don't have them.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eI love the auto-formatter. Want to lay out your struct literal into\n\tmultiple lines? Just add a trailing comma to the end of the last element.\n\tIt's very snappy, and a joy to use.\u003c/li\u003e\n\t\u003cli\u003eLike every modern programming language, Zig comes with a test framework\n\tbuilt into the language. While it's not all too important for my grand plan\n\tof having one big test that runs a bunch of replays and compares their game\n\tstates against the original binary, small tests could still be useful for\n\tprotecting gameplay code against accidental changes. It would be great if I\n\tdidn't have to evaluate and choose \u003ca\n\thref=\"https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C++\"\u003eamong\n\tthe many testing frameworks for C++\u003c/a\u003e and could just use a language\n\tstandard.\u003c/li\u003e\n\t\u003cli\u003e\u003ca\n\thref=\"https://zig.news/edyu/zig-package-manager-wtf-is-zon-558e\"\u003ePackage\n\tmanagement is still in its infancy\u003c/a\u003e, but it's looking pretty good so far,\n\tresembling Go's decentralized approach of just pointing to a URL but with\n\tspecific version selection from the get-go.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tHowever, as a version number of 0.11.0 might already suggest, the whole\n\texperience was then bogged down by quite a lot of issues:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhile Zig's C/C++ compilation feature is \u003ca\n\thref=\"https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html\"\u003every\n\twell architected to reuse the C/C++ standard libraries of GCC and MinGW and\n\tthus automatically keeps up with changes to the C++ standard library\u003c/a\u003e,\n\tit's ultimately still just a Clang frontend. If you've been working with a\n\tVisual Studio-exclusive codebase – which, as we're going to see below, can\n\teasily happen even \u003ci\u003eif\u003c/i\u003e you compile in C++23 mode – you'd now have to\n\tmigrate to Clang \u003ci\u003eand\u003c/i\u003e Zig in a single step. Obviously, this can't ever\n\tbe fixed without Microsoft open-sourcing their C++ compiler. And even then,\n\tsupporting a separate set of command-line flags might not be worth it.\u003c/li\u003e\n\t\u003cli\u003eThe standard library is very poorly documented, \u003ci\u003eespecially\u003c/i\u003e in the\n\tbuild-related parts that are meant to attract the C++ audience.\u003c/li\u003e\n\t\u003cli\u003eOften, the only documentation is found in blog posts from a few years\n\tago, with example code written against old Zig versions that doesn't compile\n\ton the newest version anymore. It's all very far from stable.\u003c/li\u003e\n\t\u003cli\u003eHowever, Zig's project generation sub-commands (\u003ccode\u003ezig\n\tinit-exe\u003c/code\u003e and friends) \u003ci\u003edo\u003c/i\u003e emit well-documented boilerplate\n\tcode? It does make sense for that code to double as a comprehensive example,\n\tbut Zig advertises itself as so simple that I didn't even think about\n\tbootstrapping my project with a CLI tool at first – unlike, say, Rust, where\n\ta project always starts with filling out a small form in\n\t\u003ccode\u003eCargo.toml\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eThere's no progress output for C/C++ compilation? Like, at all?\u003c/li\u003e\n\t\u003cli\u003eThis hurts especially because compilation times are significantly longer\n\tthan they were with Visual Studio. By default, the current Tupfile builds\n\tShuusou Gyoku in both debug and release configurations simultaneously. If I\n\tfully rebuild everything from a clean cache, Visual Studio finishes such a\n\tbuild in roughly the same amount of time that Zig takes to compile just a\n\tdebug build.\u003c/li\u003e\n\t\u003cli\u003eThe \u003ccode\u003e--global-cache-dir\u003c/code\u003e option is only supported by specific\n\tsubcommands of the \u003ccode\u003ezig\u003c/code\u003e CLI rather than being a top-level\n\tsetting, and throws an error if used for any other subcommand. Not having a\n\tsystem-wide way to change it and being forced into writing a wrapper script\n\tfor that is fine, but it would be nice if said wrapper script didn't have to\n\talso parse and switch over the subcommand just to figure out whether it is\n\tallowed to append the setting.\u003c/li\u003e\n\t\u003cli\u003ecompiler-rt still needs a bit of dead code elimination work. As soon as\n\tyour program needs a single polyfilled function, you get all of them,\n\tbecause they get referenced in some exception-related table even if nothing\n\tuses them? Changing the \u003ccode\u003elink_eh_frame_hdr\u003c/code\u003e option had no\n\teffect.\u003c/li\u003e\n\t\u003cli\u003eAnd that was not the only \u003ccode\u003estd.Build.Step.Compile\u003c/code\u003e option\n\tthat did nothing. Worse, if I just tweaked the options and changed nothing\n\tabout the code itself, Zig \u003ci\u003esimply copied a previously built executable\n\tout of its build cache into the output directory\u003c/i\u003e, as revealed by the\n\ttimestamp on the .EXE. While I am willing to believe that Zig correctly\n\tdetects that all these settings would just produce the same binary, I do not\n\tlike how this behavior inspires distrust and uncertainty in Zig's build\n\tprocess as a whole. After all, we still live in a world where \u003ci\u003eclearing\n\tthe build cache\u003c/i\u003e is way too often the solution for weird problems in\n\tsoftware, especially when using CMake. And it makes sense why it would be:\n\tIf you develop a complex system and then try solving the infamously hard\n\tproblem of cache invalidation on top, the risk of getting cache invalidation\n\twrong is, by definition, higher than if that was the only thing your system\n\tdid. That's the reason why I like Tup so much: It \u003ci\u003esolely\u003c/i\u003e focuses on\n\tgetting cache invalidation right, and rather errs on the side of caution by\n\tmaybe unnecessarily rebuilding certain files every once in a while because\n\tthe compiler may have read from an environment variable that has changed in\n\tthe meantime. But this is the one job I expect a build system to do, and Tup\n\thas been delivering for years and has become fundamentally more trustworthy\n\tas a result.\u003c/li\u003e\n\t\u003cli\u003eZig activates Clang's \u003ca\n\thref=\"https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html\"\u003eUBSan\u003c/a\u003e\n\tin debug builds by default, which executes a program-crashing\n\t\u003ccode\u003eUD2\u003c/code\u003e instruction whenever the program is about to rely on\n\tundefined C++ behavior. In theory, that's a great help for spotting hidden\n\tportability issues, but it's not helpful at all if these crashes are\n\tseemingly caused by C++ standard library code?! \u003ca\n\thref=\"https://github.com/ziglang/zig/issues/5163\"\u003eWithout any clear info\n\tabout the actual cause\u003c/a\u003e, this just turned into yet another annoyance on\n\ttop of all the others. Especially because I apparently kept searching for\n\tthe wrong terms when I first encountered this issue, and only \u003ca\n\thref=\"https://github.com/ziglang/zig/issues/4830#issuecomment-605451561\"\u003efound\n\tout how to deactivate it\u003c/a\u003e after I already decided against Zig.\u003c/li\u003e\n\t\u003cli\u003eAlso, can we get \u003ccode\u003e\u003ca\n\thref=\"https://learn.microsoft.com/en-us/cpp/build/reference/pdbaltpath-use-alternate-pdb-path?view=msvc-170\"\u003e/PDBALTPATH\u003c/a\u003e\u003c/code\u003e?\n\tBaking absolute paths from the filesystem of the developer's machine into\n\treleased binaries is not only cringe in itself, but can also cause potential\n\tprivacy or security accidents.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo for the time being, I still prefer Tup. But give it maybe two or three\n\tyears, and I'm sure that Zig will eventually become the best tool for\n\tresurrecting legacy C++ codebases. That is, if the \u003ca\n\thref=\"https://github.com/ziglang/zig/issues/16270\"\u003eproposed divorce of the\n\tcore Zig compiler from LLVM\u003c/a\u003e \u003ci\u003eisn't\u003c/i\u003e an indication that the\n\tproductive parts of the Zig community consider the C/C++ building features\n\tto be \"good enough\", and are about to de-emphasize them to focus more\n\tstrongly on the actual Zig language. Gaining adoption for your new systems\n\tlanguage by bundling it with a C/C++ build system is such a great and unique\n\tstrategy, and it almost worked in my case. And who knows, maybe Zig will\n\talready be good enough by the time I get to port PC-98 Touhou to modern\n\tsystems.\n\u003c/p\u003e\u003cp\u003e\n\t(If you came from the \u003ca\n\thref=\"https://github.com/ziglang/zig/wiki/Third-Party-Tracking-Issues-(what-is-important-to-other-people%3F)\"\u003eZig\n\twiki\u003c/a\u003e, you can stop reading here.)\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"concepts-2023-09-30\"\u003e\n\tA few remnants of the Zig experiment still remain in the final delivery. If\n\tthat experiment worked out, I would have had to immediately change the\n\texecution encoding to UTF-8, and decompile a few ASM functions exclusive to\n\tthe 8-bit rendering mode which we could have otherwise ignored. While Clang\n\tdoes support inline assembly with Intel syntax via\n\t\u003ccode\u003e-fms-extensions\u003c/code\u003e, it has trouble with \u003ccode\u003e; comments\u003c/code\u003e\n\tand instructions like \u003ccode\u003eREP STOSD\u003c/code\u003e, and if I have to touch that\n\tcode anyway… (The \u003ccode\u003eREP STOSD\u003c/code\u003e function translated into a single\n\tcall to \u003ccode\u003ememcpy()\u003c/code\u003e, by the way.)\n\u003c/p\u003e\u003cp\u003e\n\tAnother smaller issue was Visual Studio's lack of standard library header\n\thygiene, where #including some of the high-level STL features also includes\n\tmore foundational headers that Clang requires to be included separately, but\n\tI've already known about that. Instead, the biggest shocker was that Visual\n\tStudio accepts invalid syntax for a language feature as recent as \u003ci\u003eC++20\n\tconcepts\u003c/i\u003e:\n\u003c/p\u003e\u003cfigure\u003e\u003cpre\u003e\n// Defines the interface of a text rendering session class. To simplify this\n// example, it only has a single `Print(const char* str)` method.\ntemplate \u0026lt;class T\u0026gt; concept Session = requires(T t, const char* str) {\n\tt.Print(str);\n};\n\n// Once the rendering backend has started a new session, it passes the session\n// object as a parameter to a user-defined function, which can then freely call\n// any of the functions defined in the `Session` concept to render some text.\ntemplate \u0026lt;class F, class S\u0026gt; concept UserFunctionForSession = (\n\tSession\u0026lt;S\u0026gt; \u0026\u0026 requires(F f, S\u0026 s) {\n\t\t{ f(s) };\n\t}\n);\n\n// The rendering backend defines a `Prerender()` method that takes the\n// aforementioned user-defined function object. Unfortunately, C++ concepts\n// don't work like this: The standard doesn't allow `auto` in the parameter\n// list of a `requires` expression because it defines another implicit\n// template parameter. Nevertheless, Visual Studio compiles this code without\n// errors.\ntemplate \u0026lt;class T, class S\u0026gt; concept BackendAttempt = requires(\n\tT t, UserFunctionForSession\u0026lt;S\u0026gt; auto func\n) {\n\tt.Prerender(func);\n};\n\n// A syntactically correct definition would use a different constraint term for\n// the type of the user-defined function. But this effectively makes the\n// resulting concept unusable for actual validation because you are forced to\n// specify a type for `F`.\ntemplate \u0026lt;class T, class S, class F\u0026gt; concept SyntacticallyFixedBackend = (\n\tUserFunctionForSession\u0026lt;F, S\u0026gt; \u0026\u0026 requires(T t, F func) {\n\t\tt.Prerender(func);\n\t}\n);\n\n// The solution: Defining a dummy structure that behaves like a lambda as an\n// \"archetype\" for the user-defined function.\nstruct UserFunctionArchetype {\n\tvoid operator ()(Session auto\u0026 s) {\n\t}\n};\n\n// Now, the session type disappears from the template parameter list, which\n// even allows the concrete session type to be private.\ntemplate \u0026lt;class T\u0026gt; concept CorrectBackend = requires(\n\tT t, UserFunctionArchetype func\n) {\n\tt.Prerender(func);\n};\u003c/pre\u003e\u003cfigcaption\u003e\n\t\u003ca href=\"https://godbolt.org/z/r8aqsE49v\"\u003eHere's a Godbolt link, configured\n\twith both Visual Studio and Clang compilers.\u003c/a\u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWhat's this, Visual Studio's infamous delayed template parsing applied to\n\tconcepts, because they're templates as well? \u003ca\n\thref=\"https://devblogs.microsoft.com/cppblog/two-phase-name-lookup-support-comes-to-msvc/\"\u003eDidn't\n\tthey get rid of that 6 years ago?\u003c/a\u003e You would think that we've moved\n\tbeyond the age where compilers differed in their interpretation of the core\n\tlanguage, and that opting into a current C++ standard turns off any\n\tremaining antiquated behaviors…\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"tup-2023-09-30\"\u003e\n\tSo let's \u003ci\u003eactually\u003c/i\u003e get my Tup build scripts ready for compiling\n\tvendored libraries, because the\n\t\u003ca href=\"/blog/2022-09-04\"\u003e📝 previous 70 lines of Lua\u003c/a\u003e definitely\n\tweren't. For this use case, we'd like to have some notion of distinct build\n\ttargets that can have a unique set of compilation and linking flags. We'd\n\talso like to always build them in debug and release versions even if you\n\tonly intend to build your actual program in one of those versions – with the\n\tprevious system of specifying a single version for all code, Tup would\n\tdelete the other one, which forces a time-consuming and ultimately needless\n\trebuild once you switch to the other version.\n\u003c/p\u003e\u003cp\u003e\n\tThe solution I came up with treats the set of compiler command-line options\n\tlike a tree whose branches can concatenate new options and/or filter the\n\tversions that are built on this branch. In total, this is my 4\u003csup\u003eth\u003c/sup\u003e\n\tattempt at writing a compiler abstraction layer for Tup. Since \u003cspan\n\tclass=\"hovertext\" title=\"Sure, we can write them in any language that can generate a regular Tupfile, but we'd then incur an additional build dependency on that language. And don't you *dare* suggest Python.\"\u003ewe're\n\teffectively forced\u003c/span\u003e to write such layers in Lua, it will always be a\n\tbit janky, but I think I've finally arrived at a solid underlying design\n\tthat might also be interesting for others. Hence, I've split off the result\n\tinto \u003ca href=\"https://github.com/nmlgc/tupblocks\"\u003eits own separate\n\trepository and added high-level documentation and a documented example\u003c/a\u003e.\n\tAnd yes, that's a \u003ca href=\"http://code.grevit.net:8084/\"\u003eCode Nutrition\u003c/a\u003e\n\tlabel! I've wanted to add one of these ever since I first heard about the\n\tidea, since it communicates nicely how seriously such an open-source project\n\tshould be taken. Which, in this case, is actually not all \u003ci\u003etoo\u003c/i\u003e\n\tseriously, especially since development of the core Tup project has all but\n\tstagnated. If Zig does indeed get better and better at being a Clang\n\tfrontend/build system, the only niches left for Tup will be Visual\n\tStudio-exclusive projects, or retrocoding with nonstandard toolchains (i.e.,\n\tReC98). Quite ironic, given Tup's Unix heritage…\u003cbr\u003e\n\tOh, and maybe general Makefile-like tasks where you just want to run\n\tspecific programs. Maybe once the general hype swings back around and people\n\tstart demanding proper graph-based dependency tracking instead of \u003ca\n\thref=\"https://github.com/casey/just\"\u003ejust a command runner\u003c/a\u003e…\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"sdl-2023-09-30\"\u003e\n\tAlright, alternatives evaluated, build system ready, time to include SDL!\n\tOnce again, I went for Git submodules, but this time they're held together\n\tby \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/f5858791fb08af3e295d88f1ebe3b2041ec88c9b/build.bat\"\u003ea\n\tbatch file that ensures that the intended versions are checked out before\n\tstarting Tup\u003c/a\u003e. Git submodules have a bad rap mainly because of their\n\tusability issues, and such a script should \u003ci\u003ehopefully\u003c/i\u003e work around\n\tthem? Let's see how this plays out. If it ends up causing issues after all,\n\tI'll just switch to a Zig-like model of downloading and unzipping a source\n\tarchive. Since Windows comes with \u003ccode\u003ecurl\u003c/code\u003e and \u003ccode\u003etar\u003c/code\u003e\n\tthese days, this can even work without any further dependencies.\n\u003c/p\u003e\u003cp\u003e\n\tCompiling SDL from a non-standard build system requires \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/f5858791fb08af3e295d88f1ebe3b2041ec88c9b/Tupfile.lua#L28-L79\"\u003ea\n\tbit of globbing to include all the code that is being referenced\u003c/a\u003e, as\n\twell as a few linker settings, but it's ultimately not much of a big deal.\n\tI'm quite happy that it was possible at all without pre-configuring a build,\n\tbut hey, that's what maintaining a Visual Studio project file does to a\n\tproject. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tBy building SDL with the stock Windows configuration, we then end up with\n\texactly what the SDL developers want us to use… which is a DLL. You\n\t\u003ci\u003ecan\u003c/i\u003e statically link SDL, but they \u003ci\u003ereally\u003c/i\u003e don't want you to do\n\tthat. So strongly, in fact, that they \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/blob/0b9d8e679a26ee98bb055efd244c703b7dda8727/docs/README-dynapi.md\"\u003enot\n\tmerely argue how well the textbook advantages of dynamic linking have worked\n\tfor them and gamers as a whole, but implemented a whole \u003ci\u003edynamic API\u003c/i\u003e\n\tsystem that enforces overridable dynamic function loading even in static\n\tbuilds\u003c/a\u003e. Nudging developers to their preferred solution by removing most\n\tadvantages from static linking by default… that's certainly a strategy. It\n\tdefinitely fits with SDL's grassroots marketing, which is very good at\n\tpainting SDL as the industry standard and the only reliable way to keep your\n\tgame running on all originally supported operating systems. Well, at least\n\tuntil SDL\u0026nbsp;3 is so stable that SDL\u0026nbsp;2 gets deprecated and won't\n\treceive any code for new backends…\n\u003c/p\u003e\u003cp\u003e\n\tHowever, dynamic linking does make sense if you consider what SDL \u003ci\u003eis\u003c/i\u003e.\n\tOffering all those multiple rendering, input, and sound backends is what\n\tsets it apart from its more hip competition, and you want to have all of\n\tthem available at any time so that SDL can dynamically select them based on\n\twhat works best on a system. As a result, everything in SDL is being\n\treferenced somewhere, so there's no dead code for the linker to eliminate.\n\tLinking SDL statically with link-time code generation just prolongs your\n\tlink time for no benefit, even without the dynamic API thwarting any chance\n\tof SDL calls getting inlined.\u003cbr\u003e\n\tThere's one thing I still don't like about all this, though. The dynamic\n\tAPI's table references force you to include all of SDL's subsystems in the\n\tDLL even if your game doesn't need some of them. But it does fit with their\n\tintention of having \u003ccode\u003eSDL2.dll\u003c/code\u003e be swappable: If an older game\n\tstopped working because of an outdated \u003ccode\u003eSDL2.dll\u003c/code\u003e, it should be\n\tpossible for anyone to get that game working again by replacing that DLL\n\twith any newer version that was bundled with any random newer game. And\n\tsince that would fail if the newer \u003ccode\u003eSDL2.dll\u003c/code\u003e was size-optimized\n\tto not include some of the subsystems that the older game required, they\n\tsimply removed (or \u003cq\u003e\u003cq\u003ede-prioritized\u003c/q\u003e\u003c/q\u003e) the possibility altogether.\n\tMaybe that was their train of thought? You can always just use \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/releases\"\u003ethe official Windows\n\tDLL\u003c/a\u003e, whose whole point is to include everything, after all. 🤷\n\u003c/p\u003e\u003cp\u003e\n\tSo, what do we get in these 1.5\u0026nbsp;MiB? There are:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003erenderer backends for Direct3D 9/11/12, regular OpenGL, OpenGL ES 2.0,\n\tVulkan, and a software renderer,\u003c/li\u003e\n\t\u003cli\u003einput backends for DirectInput, XInput, Raw Input, and \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/tree/37dee79b74723b7021ccaa946e31872f0539df4a/src/joystick/hidapi\"\u003eall\n\tthe official game console controllers that can be connected via\n\tUSB\u003c/a\u003e,\u003c/li\u003e\n\t\u003cli\u003eand audio backends for WinMM, DirectSound, WASAPI, and direct-to-disk\n\trecording.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUnfortunately, SDL\u0026nbsp;2 also statically references some newer Windows API\n\tfunctions and therefore doesn't run on Windows 98. Since this build of\n\tShuusou Gyoku doesn't introduce any new features to the input or sound\n\tinterfaces, we can still use pbg's original DirectSound and DirectInput code\n\tfor the i586 build to keep it working with the rest of the\n\tplatform-independent game logic code, but it will start to lag behind in\n\tfeatures as soon as we add support for SC-88Pro BGM or \u003ca class=\"goal\"\n\thref=\"https://github.com/nmlgc/ssg/issues/48\"\u003emore sophisticated input\n\tremapping\u003c/a\u003e. If we do want to keep this build at the same feature level as\n\tthe SDL one, we now have a choice: Do we write new DirectInput and\n\tDirectSound code and get it done quickly but only for Shuusou Gyoku, or do\n\twe port SDL\u0026nbsp;2 to Windows 98 and benefit all other SDL\u0026nbsp;2 games as\n\twell? \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/53\"\u003eI leave\n\tthat for my backers to decide.\u003c/a\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"fps-2023-09-30\"\u003e\n\tImmediately after writing the first bits of actual SDL code to initialize\n\tthe library and create the game window, you notice that SDL makes it very\n\tsimple to gradually migrate a game. After creating the game window, you can\n\tcall \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_GetWindowWMInfo\"\u003e\u003ccode\u003eSDL_GetWindowWMInfo()\u003c/code\u003e\u003c/a\u003e\n\tto retrieve \u003ccode\u003eHWND\u003c/code\u003e and \u003ccode\u003eHINSTANCE\u003c/code\u003e handles that allow\n\tyou to continue using your original DirectDraw, DirectSound, and DirectInput\n\tcode and focus on porting one subsystem at a time.\u003cbr\u003e\n\tSadly, D3DWindower can no longer turn SDL's fullscreen mode into a windowed\n\tone, but DxWnd still works, albeit behaving a bit janky and insisting on\n\tminimizing the game whenever its window loses focus. But in exchange, the\n\tgame window can surprisingly be moved now! Turns out that the originally\n\tfixed window position had nothing to do with the way the game created its\n\tDirectDraw context, and everything to do with \u003ca\n\thref=\"https://github.com/nmlgc/ssg/commit/fa254107071d7d62d728365ffcb237972aefc482\"\u003epbg\n\tblocking the Win32 \"syscommand\" that allows a window to be moved\u003c/a\u003e. By\n\tdeleting a \u003ci\u003esystem menu\u003c/i\u003e… seriously?! Now I'm dying to hear the Raymond\n\tChen explanation for how this behavior dates back to an unfortunate decision\n\tduring the Win16 days or something.\u003cbr\u003e\n\tAs implied by that commit, I immediately backported window movability to the\n\ti586 build.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, the most important part of Shuusou Gyoku's main loop is its frame\n\trate limiter, whose Win32 version leaves a bit of room for improvement.\n\tOutside of the uncapped \u003ccode lang=\"ja\"\u003e[おまけ] DrawMode\u003c/code\u003e, the\n\toriginal main loop continuously checks whether at least 16 milliseconds have\n\telapsed since the last simulated (but not necessarily rendered) frame. And\n\tby that I mean \u003ci\u003econtinuously\u003c/i\u003e, and deliberately without using any of\n\tthe Windows system facilities to sleep the process in the meantime, \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/MAIN/MAIN.CPP#L71\"\u003eas\n\tevidenced by a commented-out \u003ccode\u003eSleep(1)\u003c/code\u003e call\u003c/a\u003e. This has two\n\timportant effects on the game:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe \u003ccode lang=\"ja\"\u003e60Fps DrawMode\u003c/code\u003e actually corresponds to a\n\tframe rate of\n\t\u003ccode\u003e(1000\u0026nbsp;/\u0026nbsp;16)\u0026nbsp;=\u0026nbsp;\u003c/code\u003e\u003cstrong\u003e62.5\u003c/strong\u003e FPS,\n\tnot 60. Since the game didn't account for the missing\n\t\u003csup\u003e2\u003c/sup\u003e/\u003csub\u003e3\u003c/sub\u003e\u0026nbsp;ms to bring the limit down to exactly 60 FPS,\n\t62.5\u0026nbsp;FPS is Shuusou Gyoku's \u003ci\u003eactual\u003c/i\u003e official frame rate in a\n\tnon-VSynced setting, which we should also maintain in the SDL port.\u003c/li\u003e\n\t\u003cli\u003eNot sleeping the process turns Shuusou Gyoku's frame rate limitation\n\tinto a busy-waiting loop, which always uses 100% of a single CPU core just\n\tto wait for the next frame. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUnsurprisingly, SDL features \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_Delay\"\u003ea delay function that properly\n\tsleeps the process for a given number of milliseconds\u003c/a\u003e. But just\n\tspecifying 16 here is not \u003ci\u003eexactly\u003c/i\u003e what we want:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eSure, modern computers are fast, but a frame won't ever take an\n\tinfinitely fast 0 milliseconds to render. So we still need to take the\n\tcurrent frame time into account.\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eSDL_Delay()\u003c/code\u003e's documentation says that the wake-up could be\n\tfurther delayed due to OS scheduling.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tTo address both of these issues, I went with a base delay time of\n\t\u003ci\u003e15\u003c/i\u003e\u0026nbsp;ms minus the time spent on the current frame, followed by\n\tbusy-waiting for the last millisecond to make sure that the next frame\n\tstarts on the exact frame boundary. And lo and behold: Even though this\n\tstill technically wastes up to 1\u0026nbsp;ms of CPU time, it still dropped CPU\n\tusage into the 0%-2% range during gameplay on my Intel Core i5-8400T CPU,\n\twhich is over 5 years old at this point. Your laptop battery will appreciate\n\tthis new build quite a bit.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"sdlaudio-2023-09-30\"\u003e\n\tTime to look at audio then, because it sure looks less complicated than\n\tinput, doesn't it? Loading sounds from .WAV file buffers, playing a fixed\n\tnumber of instances of every sound at a given position within the stereo\n\tfield and with optional looping… and that's everything already. The\n\tDirectSound implementation is so straightforward that the most complex part\n\tof its code is the .WAV file parser.\u003cbr\u003e\n\tWell, the big problem with audio is actually \u003ci\u003efinding\u003c/i\u003e a cross-platform\n\tbackend that implements these features in a way that seamlessly works with\n\tShuusou Gyoku's original files. DirectSound really is the perfect sound API\n\tfor this game:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt doesn't require the game code to specify any output sample format.\n\tJust load the individual sound effects in their original format, and\n\tplayback just works and sounds correctly.\u003c/li\u003e\n\t\u003cli\u003eIts final sound stream seems to have a latency of 10\u0026nbsp;ms, which is\n\tperfectly fine for a game running at 62.5\u0026nbsp;FPS. Even 15\u0026nbsp;ms would be\n\tOK.\u003c/li\u003e\n\t\u003cli\u003eSound effect looping? Specified by passing the\n\t\u003ccode\u003eDSBPLAY_LOOPING\u003c/code\u003e flag to\n\t\u003ccode\u003eIDirectSoundBuffer::Play()\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eStereo \u003cs\u003epanning\u003c/s\u003e balancing? One method call.\u003c/li\u003e\n\t\u003cli\u003ePlaying the same sound multiple times simultaneously from a single\n\tmemory buffer? \u003ca\n\thref=\"https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mt708944(v=vs.85)\"\u003eOne\n\tmethod call\u003c/a\u003e. (It can fail though, requiring you to copy the data after\n\tall.)\u003c/li\u003e\n\t\u003cli\u003ePausing all sounds while the game window is not focused? That's the\n\tdefault behavior, but it can be equally easily disabled with \u003ca\n\thref=\"https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ee416818(v=vs.85)#members\"\u003ejust\n\ta single per-buffer flag\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eFuture streaming of waveform BGM? No problem either. Windows Touhou has\n\talways done that, and \u003ca\n\thref=\"https://github.com/nmlgc/musicroom/blob/cf1e97209c279fbbeb210601e7466af35f9848fd/src/stream.cpp\"\u003ehere's\n\tsome code I wrote 12½ years ago\u003c/a\u003e that would even work without DirectSound\n\t8's notification feature.\u003c/li\u003e\n\t\u003cli\u003eNo further binary bloat, because it's part of the operating system.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe last point can't really be an argument against anything, but we'd still\n\tbe left with 7 other boxes that a cross-platform alternative would have to\n\ttick. We already picked SDL for our portability needs, so how does its audio\n\tsubsystem stack up? Unfortunately, not great:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt's fully DIY. All you get is a single output buffer, and you have to\n\tdo all the mixing and effect processing yourself. In other words, it's \u003ca\n\thref=\"https://discourse.libsdl.org/t/playing-wav-file-with-native-sdl2-audio/22251\"\u003ethe\n\tmasochistic approach\u003c/a\u003e to cross-platform audio.\u003c/li\u003e\n\t\u003cli\u003eThere are helper functions for \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_ConvertAudio\"\u003eresampling\u003c/a\u003e and \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_MixAudioFormat\"\u003emixing\u003c/a\u003e, but the\n\tdocumentation of the latter is full of FUD. With a disclaimer that so\n\tvehemently discourages the use of this function, what are you supposed to do\n\tif you're newly integrating SDL audio into a game? Hunt for a separate sound\n\tmixing library, even though your only quality goal is parity with stone-age\n\tDirectSound? 🙄\u003c/li\u003e\n\t\u003cli\u003eIt forces the game to explicitly define the PCM sampling rate, bit\n\tdepth, and channel count of the output buffer. You \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/blob/60070d0b3d3c55d608af58d6a74f9fa540dfb3b0/src/audio/SDL_audio.c#L1264\"\u003ecan't\n\tjust pass a \u003ccode\u003enullptr\u003c/code\u003e to \u003ccode\u003eSDL_OpenAudioDevice()\u003c/code\u003e\u003c/a\u003e,\n\tand if you pass a zeroed \u003ccode\u003eSDL_AudioSpec\u003c/code\u003e structure, SDL just \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/blob/60070d0b3d3c55d608af58d6a74f9fa540dfb3b0/src/audio/SDL_audio.c#L1211\"\u003edefaults\n\tto an unacceptable 22,050\u0026nbsp;Hz sampling rate\u003c/a\u003e, regardless of what the\n\taudio device would actually prefer. It took until \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/pull/5868\"\u003elast year for them to\n\tnotice that people would at least like to \u003ci\u003equery\u003c/i\u003e the native\n\tformat\u003c/a\u003e. But of course, this approach requires the backend to actually\n\tprovide this information – and since we've seen above that DirectSound\n\tdoesn't care, \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/blob/60070d0b3d3c55d608af58d6a74f9fa540dfb3b0/src/audio/directsound/SDL_directsound.c#L160-L168\"\u003ethe\n\tDirectSound version of this function has to actually use the more modern\n\tWASAPI, and remains unimplemented if that API is not available\u003c/a\u003e.\u003cbr\u003e\n\tStandardizing the game on a single sampling rate, bit depth, and channel\n\tcount might be a decent choice for games that consistently use a single\n\tformat for all its sounds anyway. In that case, you get to do all mixing and\n\tprocessing in that format, and the audio backend will at most do one final\n\tconversion into the playback device's native format. But in Shuusou Gyoku,\n\tmost sound effects use 22,050\u0026nbsp;Hz, the boss explosion sound effect uses\n\t11,025\u0026nbsp;Hz, and the future SC-88Pro BGM will obviously use\n\t44,100\u0026nbsp;Hz. In such a scenario, you would have to pick the highest\n\tsampling rate among all sound sources, and resample any lower-quality sounds\n\tto that rate. But if the audio device uses a different sampling rate, those\n\tlower-quality sounds would get resampled a second time.\u003cbr\u003e\n\tI know that \u003ca href=\"https://wiki.libsdl.org/SDL3/SDL_OpenAudioDevice\"\u003ethis\n\twill be fixed in SDL 3\u003c/a\u003e, but that version is still under heavy\n\tdevelopment.\u003c/li\u003e\n\t\u003cli\u003ePositives? Uh… the callback-based nature means that BGM streaming is\n\trather trivial, and would even be comparatively less complicated than with\n\tDirectSound. Having a \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_LockAudioDevice\"\u003emutex\u003c/a\u003e to prevent\n\twrites to your sound instance structures while they're being read by the\n\taudio thread is nice too.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOK, sure, but you're not \u003ci\u003esupposed\u003c/i\u003e to use it for anything more than a\n\tsingle stream of audio. SDL_mixer exists precisely to cover such non-trivial\n\tuse cases, and it even supports sound effect looping and panning with just a\n\tsingle function call! But as far as the rest of the library is concerned, it\n\tmanages to be an even bigger disappointment than raw SDL audio:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAs it sits on top of SDL's audio subsystem, it still can't just use your\n\taudio device's native sample format.\u003c/li\u003e\n\t\u003cli\u003eEven worse, it insists on \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2_mixer/Mix_OpenAudioDevice\"\u003einitializing\n\tthe audio device itself\u003c/a\u003e, and thus always needs to duplicate whatever you\n\twould do for raw SDL.\u003c/li\u003e\n\t\u003cli\u003eIt only offers a very opinionated system for streaming – and of course,\n\tits opinion is wrong. 😛 The fact that it only supports a single streaming\n\taudio track wouldn't matter all too much if you could switch to another\n\ttrack at sample precision. But since you can't, you're forced to implement\n\tlooping BGM using a single file…\u003c/li\u003e\n\t\u003cli\u003e…which brings us to the unfortunate issue of loop point definitions.\n\tAnd, perhaps most importantly, the complete lack of any way to set them\n\tthrough the API?! It doesn't take long until you come up with a theory for\n\twhy the API only offers a function to \u003ci\u003eretrieve\u003c/i\u003e loop points: The\n\t\"music\" abstraction is \u003ci\u003eso\u003c/i\u003e format-agnostic that it even supports MIDI\n\tand tracker formats where a typical loop point in PCM samples doesn't make\n\tsense. Both of these formats already have in-band ways of specifying loop\n\tpoints in their respective time units. \u003ca\n\thref=\"https://github.com/stuerp/foo_midi/blob/bb44a68bfde3913016c1004e896852882d855603/foo_midi.rc#L32-L35\"\u003eThey\n\tmight not be standardized\u003c/a\u003e, but it's still much better than usual\n\tsingle-file solutions for PCM streams where the loop point has to be stored\n\tin an out-of-band way – such as in a metadata tag or an entirely separate\n\tfile.\u003cul\u003e\n\t\t\u003cli\u003eSpeaking of MIDI, why is it so common among these APIs to not have\n\t\tany way of specifying the MIDI device? The fact that Windows Vista\n\t\tremoved the Control Panel option for specifying the system-wide default\n\t\tMIDI output device is no excuse for your API lacking the option as well.\n\t\tIn fact, your MIDI API now needs such a setting \u003ci\u003emore\u003c/i\u003e than it was\n\t\tneeded in the Windows XP and 9x days.\u003c/li\u003e\n\t\t\u003cli\u003eActually, wait, \u003ca\n\t\thref=\"https://wiki.libsdl.org/SDL2_mixer/Mix_ModMusicJumpToOrder\"\u003ethe\n\t\tAPI does have a function that is exclusive to tracker formats\u003c/a\u003e. Which\n\t\tmeans that they aren't \u003ci\u003eactually\u003c/i\u003e insisting on a clean, consistent,\n\t\tand minimal API here… 🤔\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eFunnily enough, they did once receive a patch for a function to set loop\n\tpoints which was never upstreamed… and this patch came \u003ca\n\thref=\"https://discourse.libsdl.org/t/loop-points-support-for-sdl-mixer/19586\"\u003efrom\n\tthe main developer behind PyTouhou\u003c/a\u003e, who needed that feature for obvious\n\treasons. The world sure is a small place.\u003c/li\u003e\n\t\u003cli\u003eAs a result, they turned loop points into a property that each\n\tindividual format \u003ca\n\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/49d2e332c54b39e4b5593dfb52b7dc6ac214c7b1/src/codecs/music_drflac.c#L407\"\u003emay\u003c/a\u003e\n\tor \u003ca\n\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/49d2e332c54b39e4b5593dfb52b7dc6ac214c7b1/src/codecs/music_mpg123.c#L542\"\u003emay\n\tnot\u003c/a\u003e have. Want to \u003ca\n\thref=\"https://www.thpatch.net/w/index.php?title=Touhou_Patch_Center:BGM_modding\u0026oldid=2500522#How_to_loop_MP3_files\"\u003eloop\n\tMP3 files at sample precision\u003c/a\u003e? Tough luck, time to reconvert to another\n\tlossy format. 🙄 This is the exact jank I decided against when I implemented\n\tBGM modding for thcrap back in \u003ca\n\thref=\"https://github.com/thpatch/thcrap/releases/tag/2018-12-03\"\u003e2018\u003c/a\u003e,\n\twhere I concluded that \u003ca\n\thref=\"https://github.com/Wintiger0222/uth05win/issues/10\"\u003eseparate intro and\n\tloop files are the way to go\u003c/a\u003e.\u003cbr\u003e\n\tBut OK, we only plan to use FLAC and Ogg Vorbis for the SC-88Pro BGM, for\n\twhich SDL_mixer does support loop points in the form of \u003ca\n\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/49d2e332c54b39e4b5593dfb52b7dc6ac214c7b1/src/codecs/music_drflac.c#L122-L129\"\u003eVorbis\u003c/a\u003e\n\t\u003ca\n\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/49d2e332c54b39e4b5593dfb52b7dc6ac214c7b1/src/codecs/music_ogg.c#L285-L292\"\u003ecomments\u003c/a\u003e,\n\tand hey, we can even pass them at sample accuracy. Sure, it's wrong and\n\teverything, but nothing I \u003ci\u003ecouldn't\u003c/i\u003e work with…\u003c/li\u003e\n\t\u003cli\u003eHowever, the final straw that makes SDL_mixer unsuitable for Shuusou\n\tGyoku is its core sound mixing paradigm of distributing all sound effects\n\tonto a fixed number of channels, set to \u003ca\n\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/5ce3f9268bf60a0412bfd7c46b8858f84eaec1ab/include/SDL3_mixer/SDL_mixer.h#L214-L219\"\u003e8\n\tby default\u003c/a\u003e. Which raises the quite ridiculous question of how many we\n\twould actually need to cover the maximum amount of sounds that can\n\tsimultaneously be played back in any game situation. The theoretic maximum\n\twould be 41, which is the combined sum of individual sound buffer instances\n\tof all 20 original sound effects. The practical limit would surely be a lot\n\tsmaller, but we could only find out that one through experiments, which\n\thonestly is quite a silly proposition.\u003cul\u003e\n\t\t\u003cli\u003eIt makes you wonder why they went with this paradigm in the first\n\t\tplace. And sure enough, they \u003ca\n\t\thref=\"https://github.com/libsdl-org/SDL_mixer/blob/5ce3f9268bf60a0412bfd7c46b8858f84eaec1ab/src/mixer.c#L417\"\u003eactually\n\t\tuse the aforementioned SDL core function for mixing audio\u003c/a\u003e. Yes, the\n\t\tsame function whose current documentation advises against using it for\n\t\tthis exact use case. 🙄 What's the argument here? \u003ci\u003e\"Sure, 8 is\n\t\tsignificantly more than 2, but any mixing artifacts that will occur for\n\t\tthe next 6 sounds are not worrying about, but they get \u003ci\u003ereally\u003c/i\u003e bad\n\t\tafter the 8\u003csup\u003eth\u003c/sup\u003e sound, so we're just going to protect you from\n\t\tthat\u003c/i\u003e\"? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003c/ul\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThere is \u003ca href=\"https://github.com/WohlSoft/SDL-Mixer-X\"\u003ea fork\u003c/a\u003e that\n\tdoes add support for an arbitrary number of music streams, but the rest of\n\tits features leave me questioning the priorities and focus of this project.\n\tBecause surely, when I think about missing features in an audio backend, I\n\timmediately think about \u003ca\n\thref=\"https://wohlsoft.github.io/SDL-Mixer-X/SDL_mixer_ext.html#Mix_005fLoadMUS_005fRW_005fGME\"\u003esupport\n\tfor a vast array of chiptune file formats\u003c/a\u003e… 🤪\u003cbr\u003e And wait,\n\t\u003ci\u003ewhat\u003c/i\u003e, \u003ca href=\"https://github.com/libsdl-org/SDL_mixer/pull/378\"\u003ethey\n\tmerged this piece of bloat back into the official SDL_mixer library\u003c/a\u003e?!\n\tThanks for \u003ca\n\thref=\"https://scarybeastsecurity.blogspot.com/2016/11/0day-exploit-compromising-linux-desktop.html\"\u003eopening\n\tup a vast attack surface for potential security vulnerabilities\u003c/a\u003e in code\n\tthat would never run for the majority of users, just to cover some niche\n\tformats that nobody would seriously expect in a general audio library. And\n\tthat's coming from someone who loves listening to that stuff!\u003cbr\u003e\n\tAt this rate, I'm expecting SDL_mixer to gain \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law\"\u003ea mail\n\tclient\u003c/a\u003e by the end of the decade. Hmm, what's the closest audio thing to\n\ta mail client… oh, right, WebRTC! Yeah, let's just casually \u003ca\n\thref=\"https://www.webrtc-developers.com/did-i-choose-the-right-webrtc-stack/#libwebrtc-c-the-original-one\"\u003edrop\n\ta giant part of the Chromium codebase\u003c/a\u003e into SDL_mixer, what could\n\tpossibly go wrong?\n\u003c/p\u003e\u003cp id=\"miniaudio-2023-09-30\"\u003e\n\tThis dire situation made me wonder if SDL was the wrong choice for Shuusou\n\tGyoku to begin with. Looking at other low-level cross-platform game\n\tlibraries, you'll quickly notice that \u003ci\u003eall\u003c/i\u003e of them come with mostly\n\tequally capable 2D renderers these days, and mainly differentiate themselves\n\tin minute API details that you'd only notice upon a really close look.\u003cbr\u003e\n\t\u003ca href=\"https://www.raylib.com/\"\u003eraylib\u003c/a\u003e is another one of those\n\tlibraries and has been getting exceptionally popular in recent years, to the\n\tpoint of even having more than twice as many GitHub stars as SDL. By\n\trestricting itself to OpenGL, it can even \u003ca\n\thref=\"https://www.raylib.com/cheatsheet/cheatsheet.html\"\u003eoffer an\n\tabstraction for shaders\u003c/a\u003e, which we'd \u003ci\u003ereally\u003c/i\u003e like for the \u003cspan\n\tlang=\"ja\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e lens ball effect.\u003cbr\u003e\n\tIn the case of raylib's audio system, the lack of sound effect looping is\n\tthe minute API detail that would make it annoying to use for Shuusou Gyoku.\n\tBut it might be worth a look at how raylib implements all this if it doesn't\n\tuse SDL… which turned out to be the best look I've taken in a long time,\n\tbecause raylib builds on top of \u003ca href=\"https://miniaud.io/\"\u003eminiaudio\u003c/a\u003e\n\twhich is \u003ci\u003eexactly\u003c/i\u003e the kind of audio library I was hoping to find.\n\tLet's check the list from above:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e🟢 miniaudio's high-level API initialization defaults to the native\n\tsample format of the playback device. Its internal processing uses 32-bit\n\tfloating-point samples and only converts back to the native bit depth as\n\tnecessary when writing the final stream into the backend's audio buffer.\n\tWASAPI, for example, never needs any further conversion because it operates\n\twith 32-bit floats as well.\u003c/li\u003e\n\t\u003cli\u003e🟢 The final audio stream uses the same 10\u0026nbsp;ms update period (and\n\tthus, sound effect latency) that I was getting with DirectSound.\u003c/li\u003e\n\t\u003cli\u003e🟢 Stereo \u003cs\u003epanning\u003c/s\u003e balancing? \u003ccode\u003ema_sound_set_pan()\u003c/code\u003e,\n\talthough it does require a conversion from Shuusou Gyoku's dB units into a\n\tlinear attenuation factor.\u003c/li\u003e\n\t\u003cli\u003e🟢 Sound effect looping? \u003ccode\u003ema_sound_set_looping()\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003e🟢 Playing the same sound multiple times simultaneously from a single\n\tmemory buffer? Perfectly possible, but requires a bit of digging in the\n\theader to find the best solution. \u003ca\n\thref=\"#miniaudio-instancing-2023-09-30\"\u003eMore on that below.\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e🟢 Future streaming of waveform BGM? Just call\n\t\u003ccode\u003ema_sound_init_from_file()\u003c/code\u003e with the\n\t\u003ccode\u003eMA_SOUND_FLAG_STREAM\u003c/code\u003e flag.\u003cul\u003e\n\t\t\u003cli\u003e👍 It also comes with a FLAC decoder in the core library and an Ogg\n\t\tVorbis one as part of the repo, …\u003c/li\u003e\n\t\t\u003cli\u003e🤩 … and even supports gapless switching between the intro and loop\n\t\tfiles via a single declarative call to\n\t\t\u003ccode\u003ema_data_source_set_next()\u003c/code\u003e!\u003cbr\u003e\n\t\t(Oh, and it also has \u003ccode\u003ema_data_set_loop_point_in_pcm_frames()\u003c/code\u003e\n\t\tfor anyone who still believes in \u003ci\u003eobviously\u003c/i\u003e and \u003ci\u003eobjectively\u003c/i\u003e\n\t\tinferior out-of-band loop points.)\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e🟢 Pausing all sounds while the game window is not focused? It's not\n\tautomatic, but adding new functions to the sound interface and calling\n\t\u003ccode\u003ema_engine_stop()\u003c/code\u003e and \u003ccode\u003ema_engine_start()\u003c/code\u003e does the\n\ttrick, and most importantly doesn't cause any samples to be lost in the\n\tprocess.\u003c/li\u003e\n\t\u003cli\u003e🟡 Sound control is implemented in a lock-free way, allowing your main\n\tgame thread to call these at any time without causing glitches on the audio\n\tthread. While that looks nice and optimal on the surface, you now have to\n\teither believe in the soundness (ha) of the implementation, or verify that\n\tatomic structure fields actually \u003ci\u003eare\u003c/i\u003e enough to not cause any race\n\tconditions (which I did for the calls that Shuusou Gyoku uses, and I didn't\n\tfind any). \u003ci\u003e\"It's all lock-free, don't worry about it\"\u003c/i\u003e might be\n\t\u003ci\u003eeasier\u003c/i\u003e, but I consider SDL's approach of just providing a mutex to\n\tprevent the output callback from running while you mutate the sound state to\n\tactually be \u003ci\u003esimpler\u003c/i\u003e conceptually.\u003c/li\u003e\n\t\u003cli\u003e🟡 miniaudio adds 247\u0026nbsp;KB to the binary in its minimum\n\tconfiguration, a bit more than expected. Some of that is bloat from effect\n\tcode that we never use, but it does include backends for all three Windows\n\taudio subsystems (WASAPI, DirectSound, and WinMM).\u003c/li\u003e\n\t\u003cli\u003e✅ But perhaps most importantly: It natively supports all modern\n\toperating systems that one could seriously want to port this game to, and\n\tcould be easily ported to any other backend, \u003ca\n\thref=\"https://miniaud.io/docs/examples/custom_backend.html\"\u003eincluding\n\tSDL\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOh, and it's written by the same developer who also wrote the best FLAC\n\tlibrary back in 2018. And that's despite them being single-file C libraries,\n\twhich I consider to be massively overrated…\n\u003c/p\u003e\u003cp\u003e\n\tThe drawback? Similar to Zig, it's only on version 0.11.18, and also focuses\n\ton good high-level documentation at the expense of an API reference. Unlike\n\tZig though, the three issues I ran into turned out to be actual and fixable\n\tbugs: \u003ca href=\"https://github.com/mackron/miniaudio/issues/716\"\u003eTwo minor\n\tones related to looping of streamed sounds shorter than 2 seconds which\n\twon't ever actually affect us before we get into BGM modding\u003c/a\u003e, and \u003ca\n\thref=\"https://github.com/mackron/miniaudio/pull/722\"\u003ea critical one that\n\tadded high-frequency corruption to any mono sound effect during its\n\texpansion to stereo\u003c/a\u003e. The latter took days to track down – with symptoms\n\tlike these, you'd immediately suspect the bug to lie in the resampler or its\n\tlow-pass filter, both of which are so much more of a fickle and configurable\n\tpart of the conversion chain here. Compared to that, stereo expansion is so\n\tconceptually simple that you wouldn't imagine anyone getting it wrong.\u003cbr\u003e\n\tWhile the latter PR has been merged, the fix is still only part of the\n\t\u003ccode\u003edev\u003c/code\u003e branch and hasn't been properly released yet. Fortunately,\n\traylib is not affected by this bug: It does \u003ca\n\thref=\"https://github.com/raysan5/raylib/commit/3a3e672804d7c0efb429d45b22554b357c0dc11d\"\u003ecurrently\n\tship version 0.11.16 of miniaudio\u003c/a\u003e, but its usage of the library predates\n\tminiaudio's high-level API and it therefore uses a different,\n\tnon-SSE-optimized code path for its format conversions.\n\u003c/p\u003e\u003cp id=\"miniaudio-instancing-2023-09-30\"\u003e\n\tThe only slightly tricky part of implementing a miniaudio backend for\n\tShuusou Gyoku lies in setting up multiple simultaneously playing instances\n\tfor each individual sound. The documentation and answers on the issue\n\ttracker heavily push you toward miniaudio's resource manager and its file\n\tabstractions to handle this use case. We surely could turn Shuusou Gyoku's\n\tnumeric sound effect IDs into fake file names, but it doesn't really fit the\n\texisting architecture where the sound interface just receives in-memory .WAV\n\tfile buffers loaded from the \u003ccode\u003eSOUND.DAT\u003c/code\u003e packfile.\u003cbr\u003e\n\tIn that case, this seems to be the best way:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eCall \u003ccode\u003ema_decode_memory()\u003c/code\u003e to decode from any of the supported\n\taudio formats to a buffer of raw PCM samples.\u003cbr\u003e At this point, you can\n\tchoose between\u003col\u003e\n\t\t\u003cli\u003edecoding into the original format the sound effect is stored in,\n\t\twhich would require it to be converted to the playback format every\n\t\ttime it's played, or\u003c/li\u003e\n\t\t\u003cli\u003edecoding into 32-bit floats (the native bit depth of the miniaudio\n\t\tengine) and the native sampling rate of the playback device, which\n\t\tavoids any further resampling and floating-point conversion, but takes\n\t\tup more memory.\u003c/li\u003e\n\t\u003c/ol\u003eNowadays, it's not clear at all which of the two approaches is faster.\n\tDoes it actually matter if we save the audio thread from doing all those\n\tfloating-point operations on every sample? Or is that no longer true these\n\tdays because the audio thread is probably running on a different CPU core,\n\tthe rest of the game largely doesn't touch the floating-point parts of your\n\tCPU anyway, and you'd rather want to keep sound effects small so that they\n\tcan better fit into the CPU cache? That would be an interesting question to\n\tbenchmark, but just like the similar text rendering question from the last\n\tblog posts, \u003ci\u003eit doesn't matter for this tiny 2000s retro game\u003c/i\u003e. 😌\n\t\u003cbr\u003e\n\tI went with 2) mainly because it simplified all the debugging I was doing.\n\tAt a sampling rate of 48,000\u0026nbsp;Hz, this increases the memory usage for\n\tall sound effects from 379\u0026nbsp;KiB to 3.67\u0026nbsp;MiB. At least I'm not\n\tchannel-expanding all sound effects as well here…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e We've seen earlier that mono➜stereo expansion\n\tis SSE-optimized, so it's very hard to justify a further doubling of the\n\tmemory usage here.\u003c/li\u003e\n\t\u003cli\u003eThen, for each instance of the sound, call\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003ema_audio_buffer_ref_init()\u003c/code\u003e to create a \u003ci\u003ereference\n\t\tbuffer\u003c/i\u003e with its own playback cursor, and\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003ema_sound_init_from_data_source()\u003c/code\u003e to create a new\n\t\thigh-level sound node that will play back the reference buffer.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp id=\"resampling-2023-09-30\"\u003e\n\tAs a side effect of hunting that one critical bug in miniaudio, I've now\n\tlearned a fair bit about audio resampling in general. You'll probably need\n\tsome knowledge about \u003ca href=\"https://xiph.org/video/vid2.shtml\"\u003ebasic\n\tdigital signal behavior\u003c/a\u003e to follow this section, and that video is still\n\tprobably the best introduction to the topic.\n\u003c/p\u003e\u003cp\u003e\n\tSo, how could this ever be an issue? The only time I ever consciously\n\tthought about resampling used to be in the context of the Opus codec and its\n\tenforced sampling rate of 48,000\u0026nbsp;Hz, and how \u003ca\n\thref=\"https://hydrogenaud.io/index.php/topic,97051.0.html\"\u003eOpus advocates\n\tclaim that resampling is a solved problem and nothing to worry about,\n\tespecially in the context of a lossy codec\u003c/a\u003e. Still, I didn't add Opus to\n\tthcrap's BGM modding feature entirely because the mere thought of having to\n\tdownsample to 44,100\u0026nbsp;Hz in the decoder was off-putting enough. But even\n\t\u003ci\u003eif\u003c/i\u003e my worries were unfounded in that specific case: Recording the\n\tStereo Mix of Shuusou Gyoku's now two audio backends revealed that\n\tapparently not every audio processing chain features an Opus-quality\n\tresampler…\n\u003c/p\u003e\u003cp\u003e\n\tIf we take a look at the material that resamplers actually have to work with\n\there, it quickly becomes obvious why their results are so varied. As\n\tmentioned above, Shuusou Gyoku's sound effects use rather low sampling rates\n\tthat are pretty far away from the 48,000\u0026nbsp;Hz your audio device is most\n\tdefinitely outputting. Therefore, any potential imaging noise across the\n\textended high-frequency range – i.e., from the original Nyquist frequencies\n\tof 11,025\u0026nbsp;Hz/5,512.5\u0026nbsp;Hz up to the new limit of 24,000\u0026nbsp;Hz – is\n\tstill within the audible range of most humans and can clearly color the\n\tresulting sound.\u003cbr\u003e\n\tBut it gets worse if the audio data you put into the resampler is\n\tobjectively defective to begin with, which is exactly the problem we're\n\tfacing with over half of Shuusou Gyoku's sound effects. Encoding them all as\n\t8-bit PCM is definitely excusable because it was the turn of the millennium\n\tand the resulting noise floor is masked by the BGM anyway, but the blatant\n\tclipping and DC offsets definitely aren't:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\u003cdiv id=\"waveform-2023-09-30\" class=\"clipped\"\u003e\n\t\u003ctable\u003e\u003cthead\u003e\u003ctr\u003e\n\t\t\u003ctd colspan=\"20\" style=\"height: 38px; border-bottom: unset;\"\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003cth style=\"min-width:  14px;\" data-name=\"KEBARI\"\u003eKEBARI\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 451px;\" data-name=\"TAME\"\u003eTAME\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 152px;\" data-name=\"LASER\"\u003eLASER\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  91px;\" data-name=\"LASER2\"\u003eLASER2\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  75px;\" data-name=\"BOMB\"\u003eBOMB\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  16px;\" data-name=\"SELECT\"\u003eSELECT\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  15px;\" data-name=\"HIT\"\u003eHIT\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  15px;\" data-name=\"CANCEL\"\u003eCANCEL\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 182px;\" data-name=\"WARNING\"\u003eWARNING\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 149px;\" data-name=\"SBLASER\"\u003eSBLASER\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  15px;\" data-name=\"BUZZ\"\u003eBUZZ\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  75px;\" data-name=\"MISSILE\"\u003eMISSILE\u003c/th\u003e\n\t\t\u003cth style=\"min-width:  17px;\" data-name=\"JOINT\"\u003eJOINT\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 151px;\" data-name=\"DEAD\"\u003eDEAD\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 225px;\" data-name=\"SBBOMB\"\u003eSBBOMB\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 753px;\" data-name=\"BOSSBOMB\"\u003eBOSSBOMB\u003c/th\u003e\n\t\t\u003cth style=\"min-width:   9px;\" data-name=\"ENEMYSHOT\"\u003eENEMYSHOT\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 121px;\" data-name=\"HLASER\"\u003eHLASER\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 376px;\" data-name=\"TAMEFAST\"\u003eTAMEFAST\u003c/th\u003e\n\t\t\u003cth style=\"min-width: 120px;\" data-name=\"WARP\"\u003eWARP\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-00.png?5f2cf144\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 1/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-01.png?c6dc800f\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 2/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-02.png?d191c282\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 3/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-03.png?79f3e3ea\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 4/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-04.png?43e5886e\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 5/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-05.png?b0413066\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 6/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-06.png?accd46d0\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 7/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-07.png?f9823e5a\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 8/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-08.png?abe7a296\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 9/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-09.png?6c4f86b7\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 10/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-10.png?675381d7\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 11/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-11.png?d28b449d\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 12/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-12.png?cac1df65\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 13/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-13.png?4c38eb8a\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 14/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-14.png?3f2ee4b8\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 15/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-15.png?6224bf32\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 16/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-16.png?7fb9ed1c\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 17/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-17.png?5615dc01\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 18/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-18.png?2e46b9d6\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 19/20\"\n\t\t\u003e\u003c/td\u003e\u003ctd\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-09-30-SH01-SOUND.DAT-19.png?ce5417f6\" alt=\"\u003ccode\u003eSOUND.DAT\u003c/code\u003e, file 20/20\"\n\t\t\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003ctfoot\u003e\u003ctr\u003e\n\t\t\u003ctd colspan=\"20\" style=\"height: 53px;\"\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tfoot\u003e\u003c/table\u003e\n\u003c/div\u003e\u003cfigcaption\u003e\n\tWaveforms for all 20 of Shuusou Gyoku's sound effects, in the order they\n\tappear inside \u003ccode\u003eSOUND.DAT\u003c/code\u003e and with their internal names. We can\n\tsee quite an abundance of \u003cspan style=\"color: red\"\u003eclipping\u003c/span\u003e, as well\n\tas a significant \u003ca href=\"https://en.wikipedia.org/wiki/DC_bias\"\u003eDC\n\toffset\u003c/a\u003e in \u003ccode\u003eWARNING\u003c/code\u003e, \u003ccode\u003eBUZZ\u003c/code\u003e, \u003ccode\u003eJOINT\u003c/code\u003e,\n\t\u003ccode\u003eSBBOMB\u003c/code\u003e, and \u003ccode\u003eBOSSBOMB\u003c/code\u003e.\n\t\u003cform\u003e\n\t\t\u003cinput type=\"checkbox\" id=\"peak-toggle-2023-09-30\" onchange=\"\n\t\t\tconst container = document.getElementById('waveform-2023-09-30');\n\t\t\tif(this.checked) {\n\t\t\t\tcontainer.classList.remove('clipped');\n\t\t\t} else {\n\t\t\t\tcontainer.classList.add('clipped');\n\t\t\t}\n\t\t\"\u003e\n\t\t\u003clabel for=\"peak-toggle-2023-09-30\"\u003eShow true peaks\u003c/label\u003e\n\t\u003c/form\u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWait a moment, \u003ci\u003etrue peaks\u003c/i\u003e? Where do those come from? And, equally\n\timportantly, how can we even observe, measure, and store \u003ci\u003eanything\u003c/i\u003e\n\tabove the maximum amplitude of a digital signal?\n\u003c/p\u003e\u003cp\u003e\n\tThe answer to the first question can be directly derived from the Xiph.org\n\tvideo I linked above: Digital signals are lollipop graphs, not stairsteps as\n\tcommonly depicted in audio editing software. Converting them back to an\n\tanalog signal involves constructing a continuous curve that passes through\n\teach sample point, and whose frequency components stay below the Nyquist\n\tfrequency. And if the amplitude of that reconstructed wave changes too\n\tstrongly and too rapidly, the resulting curve can easily overshoot the\n\tmaximum digital amplitude of \u003ca href=\"https://en.wikipedia.org/wiki/DBFS\"\u003e0\n\tdBFS\u003c/a\u003e even if none of the defined samples are above that limit.\n\u003c/p\u003e\u003cp\u003e\n\tBut I can assure you that I did not create the waveform images above by\n\trecording the analog output of some speakers or headphones and then matching\n\tthe levels to the original files, so how did I end up with that image? It's\n\tnot an Audacity feature either because \u003ca\n\thref=\"https://forum.audacityteam.org/t/29575/4\"\u003ethe development team argues\n\tthat there is no \"true waveform\" to be visualized as every DAC behaves\n\tdifferently\u003c/a\u003e. While this is correct in \u003ci\u003etheory\u003c/i\u003e, we'd be happy just\n\tto get a rough approximation here.\u003cbr\u003e\n\tffmpeg's \u003ccode\u003eebur128\u003c/code\u003e filter has a parameter to measure the true\n\tpeak of a waveform and fairly understandable source code, and once I looked\n\tat it, all the pieces suddenly started to make sense: For our purpose of\n\tonly looking at digital signals, 💡 \u003ci\u003eresampling to a floating-point signal\n\twith an infinite sampling rate is equivalent to a DAC\u003c/i\u003e. And that's\n\texactly what this filter does: It \u003ca\n\thref=\"https://github.com/FFmpeg/FFmpeg/blob/9ef20920ab82c46de095499deec2777b48a19370/libavfilter/f_ebur128.c#L492-L514\"\u003epicks\n\t192,000\u0026nbsp;Hz and 64-bit float\u003c/a\u003e as a format that's close enough to the\n\tideal of \"analog infinity\" \u003ca\n\thref=\"https://people.xiph.org/~xiphmont/demo/neil-young.html\"\u003efor all\n\tpractical purposes that involve digital audio\u003c/a\u003e, and then simply \u003ca\n\thref=\"https://github.com/FFmpeg/FFmpeg/blob/9ef20920ab82c46de095499deec2777b48a19370/libavfilter/f_ebur128.c#L642-L644\"\u003econverts\n\teach incoming 100\u0026nbsp;ms of audio\u003c/a\u003e and \u003ca\n\thref=\"https://github.com/FFmpeg/FFmpeg/blob/9ef20920ab82c46de095499deec2777b48a19370/libavfilter/f_ebur128.c#L647-L656\"\u003ekeeps\n\tthe sample with the largest floating-point value\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tSo let's store the resampled output as a FLAC file and load it into Audacity\n\tto visualize the clipped peaks… only to find all of them replaced with the\n\ttypical kind of clipping distortion? 😕 Turns out that I've stumbled over\n\tthe one case where the FLAC format \u003ci\u003eisn't\u003c/i\u003e lossless and there's\n\t\u003ci\u003eactually\u003c/i\u003e no alternative to .WAV: FLAC just doesn't support\n\tfloating-point samples and simply truncates them to discrete integers during\n\tencoding. When we measured inter-sample peaks above, we weren't only\n\tresampling to a floating-point format to avoid any quantization to discrete\n\tinteger values, but also to make it possible to store amplitudes beyond the\n\t0\u0026nbsp;dBFS point of ±1.0 in the first place. Once we lose that ability,\n\tthese amplitudes are clipped to the maximum value of the integer bit depth,\n\tand baked into the waveform with no way to get rid of them again. After all,\n\tthe resampled file now uses a higher sampling rate, and the clipping\n\tdistortion is now a defined part of what the sound \u003ci\u003eis\u003c/i\u003e.\u003cbr\u003e\n\tFinally, storing a digital signal with inter-sample peaks in a\n\tfloating-point format also makes it possible for \u003ci\u003eyou\u003c/i\u003e to reduce the\n\tvolume, which moves these peaks back into the regular, unclipped amplitude\n\trange. This is especially relevant for Shuusou Gyoku as you'll probably\n\tnever listen to sound effects at full volume.\n\u003c/p\u003e\u003cp\u003e\n\tNow that we understand what's going on there, we can finally compare the\n\toutput of various resamplers and pick a suitable one to use with miniaudio.\n\tAnd immediately, we see how they fall into two categories:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eHigh-quality resamplers are the ones I described earlier: They cleanly\n\trecreate the signal at a higher sampling rate from its raw frequency\n\trepresentation and thus add no high-frequency noise, but can lead to\n\tinter-sample peaks above 0\u0026nbsp;dBFS.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eLinear\u003c/i\u003e resamplers use much simpler math to merely interpolate\n\tbetween neighboring samples. Since the newly interpolated samples can only\n\tever stay within 0\u0026nbsp;dBFS, this approach fully avoids inter-sample\n\tclipping, but at the expense of adding high-frequency imaging noise that has\n\tto then be removed using a low-pass filter.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tminiaudio only comes with a linear resampler – but so does DirectSound as it\n\tturns out, so we can get actually pretty close to how the game sounded\n\toriginally:\n\u003c/p\u003e\u003cfigure style=\"width: 1396px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tAll of Shuusou Gyoku's sound effects combined and resampled into a\n\t\tsingle 48,000\u0026nbsp;Hz\u0026nbsp;/ 32-bit float .WAV file, using \u003ca\n\t\thref=\"https://goldwave.com/\"\u003eGoldWave\u003c/a\u003e's File Merger tool. By\n\t\tconverting to 32-bit float first and \u003ci\u003ethen\u003c/i\u003e resampling, the\n\t\tconversion preserved the exact frequency range of the original\n\t\t22,050\u0026nbsp;Hz and 11,025\u0026nbsp;Hz files, even despite clipping. There\n\t\t\u003ci\u003eare\u003c/i\u003e small noise peaks across the entire frequency range, but they\n\t\tonly occur at the exact boundary between individual sound effects. These\n\t\tare a simple result of the discontinuities that naturally occur in the\n\t\twaveform when concatenating signals that don't start or end at a 0\n\t\tsample.\u003cbr\u003e\n\t\tAs mentioned above, you'll only get this sound out of your DAC at lower\n\t\tvolumes where all of the resampled peaks still fit within 0\u0026nbsp;dBFS.\n\t\tBut you most likely will have reduced your volume anyway, because these\n\t\teffects would be ear-splittingly loud otherwise.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe result of converting 1️⃣ into FLAC. The necessary bit depth\n\t\tconversion from 32-bit float to 16-bit integers clamps any data above\n\t\t0\u0026nbsp;dBFS or \u003ccode\u003e±1.0f\u003c/code\u003e to the discrete\n\t\t[-32,678;\u0026nbsp;32,767] range, the maximum value of such\n\t\tan integer. The resulting straight lines at maximum amplitude in the\n\t\ttime domain then turn into distortion across the entire 24,000\u0026nbsp;Hz\n\t\tfrequency domain, which then remains a part of the waveform even at\n\t\tlower volumes. The locations of the high-frequency noise exactly match\n\t\tthe clipped locations in the time-domain waveform images above.\u003cbr\u003e\n\t\tThe resulting additional distortion can be best heard in\n\t\t\u003ccode\u003eBOSSBOMB\u003c/code\u003e, where the low source frequency ensures that any\n\t\tdistortion stays firmly within the hearing range of most humans.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tAll of Shuusou Gyoku's sound effects as played through DirectSound and\n\t\trecorded through Stereo Mix. DirectSound also seems to use a linear\n\t\tlow-pass filter that leaves quite a bit of high-frequency noise in the\n\t\tsignals, making these effects sound crispier than they should be.\n\t\tDepending on where you stand, this is either highly inaccurate and\n\t\tsomething that should be fixed, or actually good because the sound\n\t\teffects really benefit from that added high end. I myself am definitely\n\t\tin the latter camp – and hey, this sound is the result of original game\n\t\tcode, so it \u003ci\u003eis\u003c/i\u003e accurate at least in that regard.\n\t\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tAll of Shuusou Gyoku's sound effects as converted by miniaudio and\n\t\tdirectly saved to a file, with the same low-pass filter setting used in\n\t\tthe P0256 build. This first-order low-pass filter is a decent\n\t\tapproximation of DirectSound's resampler, even though it sounds slightly\n\t\tcrispier as the high-frequency noise is boosted a little further. By\n\t\tdefault, miniaudio would use a 4\u003csup\u003eth\u003c/sup\u003e-order low-pass filter, so\n\t\tthis is the second-lowest resampling quality you can get, short of\n\t\tdisabling the low-pass filter altogether.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tConversion results when using miniaudio's 8\u003csup\u003eth\u003c/sup\u003e-order low-pass\n\t\tfilter for resampling, the highest quality supported. This is the\n\t\tclosest we can get to the reference conversion without using a custom\n\t\tresampler. If we do want to go for perfect accuracy though, we might as\n\t\twell \u003ca class=\"goal\" href=\"https://github.com/nmlgc/ssg/issues/50\"\u003ego\n\t\tfor 1️⃣ directly\u003c/a\u003e?\n\t\u003c/div\u003e\u003chr\u003e\u003cspan\u003e\n\t\tThese spectrum images were initially created using ffmpeg's \u003ccode\u003e-lavfi\n\t\tshowspectrumpic=mode=combined:s=1280x720\u003c/code\u003e filter. The samples\n\t\tappear in the same order as in the \u003ca\n\t\thref=\"#waveform-2023-09-30\"\u003ewaveform above\u003c/a\u003e.\n\t\u003c/span\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-09-30-SH01-SE-48000-Reference-wav.hd.webp?8a378cbe\" preload=\"none\" controls data-title=\"Reference conversion, float\" loop width=\"1396\" height=\"746\" data-fps=\"60\" data-frame-count=\"1206\" style=\"aspect-ratio: 1396 / 746\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-Reference-wav.hd.avi?b0a487d6\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-09-30-SH01-SE-48000-Reference-wav.hd.webm?3270aba6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-09-30-SH01-SE-48000-Reference-wav.hd.webm?5cf4f905\" type=\"video/webm\"\u003eFrequency spectrum of all of Shuusou Gyoku's sound effects if they were perfectly resampled to 48,000\u0026nbsp;Hz. \u003ca href=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-Reference-wav.hd.avi?b0a487d6\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eKEBARI\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"\u003ccode\u003eTAME\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"\u003ccode\u003eLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"246\" data-title=\"\u003ccode\u003eLASER2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"282\" data-title=\"\u003ccode\u003eBOMB\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"330\" data-title=\"\u003ccode\u003eWARNING\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"402\" data-title=\"\u003ccode\u003eSBLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"468\" data-title=\"\u003ccode\u003eMISSILE\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"505\" data-title=\"\u003ccode\u003eDEAD\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"565\" data-title=\"\u003ccode\u003eSBBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"656\" data-title=\"\u003ccode\u003eBOSSBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"955\" data-title=\"\u003ccode\u003eENEMYSHOT\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"958\" data-title=\"\u003ccode\u003eHLASER\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1006\" data-title=\"\u003ccode\u003eTAMEFAST\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1156\" data-title=\"\u003ccode\u003eWARP\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-09-30-SH01-SE-48000-Reference-FLAC.hd.webp?8628bc3f\" preload=\"none\" controls data-title=\"Reference conversion, FLAC\" loop width=\"1396\" height=\"746\" data-fps=\"60\" data-frame-count=\"1206\" style=\"aspect-ratio: 1396 / 746\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-Reference-FLAC.hd.avi?ef12609e\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-09-30-SH01-SE-48000-Reference-FLAC.hd.webm?ccc930d5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-09-30-SH01-SE-48000-Reference-FLAC.hd.webm?5039bad4\" type=\"video/webm\"\u003eFrequency spectrum of all of Shuusou Gyoku's sound effects if they were perfectly resampled to 48,000\u0026nbsp;Hz, but converted from floating-point samples to an integer bit depth which clamps any inter-sample peaks to the maximum amplitude, thus permanently baking the resulting distortion into the file. \u003ca href=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-Reference-FLAC.hd.avi?ef12609e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eKEBARI\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"\u003ccode\u003eTAME\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"\u003ccode\u003eLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"246\" data-title=\"\u003ccode\u003eLASER2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"282\" data-title=\"\u003ccode\u003eBOMB\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"330\" data-title=\"\u003ccode\u003eWARNING\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"402\" data-title=\"\u003ccode\u003eSBLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"468\" data-title=\"\u003ccode\u003eMISSILE\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"505\" data-title=\"\u003ccode\u003eDEAD\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"565\" data-title=\"\u003ccode\u003eSBBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"656\" data-title=\"\u003ccode\u003eBOSSBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"955\" data-title=\"\u003ccode\u003eENEMYSHOT\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"958\" data-title=\"\u003ccode\u003eHLASER\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1006\" data-title=\"\u003ccode\u003eTAMEFAST\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1156\" data-title=\"\u003ccode\u003eWARP\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-09-30-SH01-SE-48000-DirectSound.hd.webp?97a0829d\" preload=\"none\" controls data-title=\"DirectSound\" loop data-active width=\"1396\" height=\"746\" data-fps=\"60\" data-frame-count=\"1206\" style=\"aspect-ratio: 1396 / 746\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-DirectSound.hd.avi?7b4a4033\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-09-30-SH01-SE-48000-DirectSound.hd.webm?7d36bfcd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-09-30-SH01-SE-48000-DirectSound.hd.webm?d949af8b\" type=\"video/webm\"\u003eFrequency spectrum of all of Shuusou Gyoku's sound effects, resampled to 48,000\u0026nbsp;Hz and recorded through the game's original DirectSound backend. \u003ca href=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-DirectSound.hd.avi?7b4a4033\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eKEBARI\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"\u003ccode\u003eTAME\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"\u003ccode\u003eLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"246\" data-title=\"\u003ccode\u003eLASER2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"282\" data-title=\"\u003ccode\u003eBOMB\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"330\" data-title=\"\u003ccode\u003eWARNING\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"402\" data-title=\"\u003ccode\u003eSBLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"468\" data-title=\"\u003ccode\u003eMISSILE\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"505\" data-title=\"\u003ccode\u003eDEAD\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"565\" data-title=\"\u003ccode\u003eSBBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"656\" data-title=\"\u003ccode\u003eBOSSBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"955\" data-title=\"\u003ccode\u003eENEMYSHOT\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"958\" data-title=\"\u003ccode\u003eHLASER\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1006\" data-title=\"\u003ccode\u003eTAMEFAST\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1156\" data-title=\"\u003ccode\u003eWARP\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-09-30-SH01-SE-48000-miniaudio-LPF-1.hd.webp?d394352e\" preload=\"none\" controls data-title=\"miniaudio, LPF\u0026nbsp;1\" loop width=\"1396\" height=\"746\" data-fps=\"60\" data-frame-count=\"1206\" style=\"aspect-ratio: 1396 / 746\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-miniaudio-LPF-1.hd.avi?7777b95b\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-09-30-SH01-SE-48000-miniaudio-LPF-1.hd.webm?f3f119fa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-09-30-SH01-SE-48000-miniaudio-LPF-1.hd.webm?82dfab7c\" type=\"video/webm\"\u003eFrequency spectrum of all of Shuusou Gyoku's sound effects resampled to 48,000\u0026nbsp;Hz, as rendered by the new miniaudio-based backend. Shows off the slight additional high-frequency content added by miniaudio's resampler in comparison to the original DirectSound backend. \u003ca href=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-miniaudio-LPF-1.hd.avi?7777b95b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eKEBARI\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"\u003ccode\u003eTAME\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"\u003ccode\u003eLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"246\" data-title=\"\u003ccode\u003eLASER2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"282\" data-title=\"\u003ccode\u003eBOMB\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"330\" data-title=\"\u003ccode\u003eWARNING\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"402\" data-title=\"\u003ccode\u003eSBLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"468\" data-title=\"\u003ccode\u003eMISSILE\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"505\" data-title=\"\u003ccode\u003eDEAD\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"565\" data-title=\"\u003ccode\u003eSBBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"656\" data-title=\"\u003ccode\u003eBOSSBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"955\" data-title=\"\u003ccode\u003eENEMYSHOT\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"958\" data-title=\"\u003ccode\u003eHLASER\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1006\" data-title=\"\u003ccode\u003eTAMEFAST\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1156\" data-title=\"\u003ccode\u003eWARP\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-09-30-SH01-SE-48000-miniaudio-LPF-8.hd.webp?0307bb54\" preload=\"none\" controls data-title=\"miniaudio, LPF\u0026nbsp;8\" loop width=\"1396\" height=\"746\" data-fps=\"60\" data-frame-count=\"1206\" style=\"aspect-ratio: 1396 / 746\" data-audio data-lossless=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-miniaudio-LPF-8.hd.avi?159b432b\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-09-30-SH01-SE-48000-miniaudio-LPF-8.hd.webm?de076c24\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-09-30-SH01-SE-48000-miniaudio-LPF-8.hd.webm?e2dd9d7a\" type=\"video/webm\"\u003eFrequency spectrum of all of Shuusou Gyoku's sound effects resampled to 48,000\u0026nbsp;Hz using miniaudio's 8\u003csup\u003eth\u003c/sup\u003e-order low-pass filter, the highest quality level supported. \u003ca href=\"/blog/static/video/zmbv/2023-09-30-SH01-SE-48000-miniaudio-LPF-8.hd.avi?159b432b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"\u003ccode\u003eKEBARI\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"6\" data-title=\"\u003ccode\u003eTAME\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"186\" data-title=\"\u003ccode\u003eLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"246\" data-title=\"\u003ccode\u003eLASER2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"282\" data-title=\"\u003ccode\u003eBOMB\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"330\" data-title=\"\u003ccode\u003eWARNING\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"402\" data-title=\"\u003ccode\u003eSBLASER\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"468\" data-title=\"\u003ccode\u003eMISSILE\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"505\" data-title=\"\u003ccode\u003eDEAD\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"565\" data-title=\"\u003ccode\u003eSBBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"656\" data-title=\"\u003ccode\u003eBOSSBOMB\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"955\" data-title=\"\u003ccode\u003eENEMYSHOT\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"958\" data-title=\"\u003ccode\u003eHLASER\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1006\" data-title=\"\u003ccode\u003eTAMEFAST\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1156\" data-title=\"\u003ccode\u003eWARP\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd yes, these are indeed the first videos on this blog to have sound! I\n\tspent another push on preparing the\n\t\u003ca href=\"/blog/2022-10-31\"\u003e📝 video conversion pipeline\u003c/a\u003e for audio\n\tsupport, and on adding the highly important volume control to the player.\n\tWeb video codecs only support lossy audio, so the sound in these videos will\n\tnot exactly match the spectrum image, but the lossless source files do\n\tcontain the original audio as uncompressed PCM streams.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"sdlinput-2023-09-30\"\u003e\n\tCompared to that whole mess of signals and noise, keyboard and joypad input\n\tis indeed much simpler. Thanks to SDL, it's \u003ci\u003ealmost\u003c/i\u003e trivial, and only\n\tslightly complicated because SDL offers two subsystems with seemingly\n\tidentical APIs:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSDL_Joystick simply numbers all axes and buttons of a joypad and \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_JoystickGetAxis\"\u003edoesn't assign any\n\tmeaning to these numbers\u003c/a\u003e, but works with every joypad ever.\u003c/li\u003e\n\t\u003cli\u003eSDL_GameController provides a consistent interface for the typical kind\n\tof modern gamepad with two analog sticks, a D-pad, and at least 4 face and 2\n\tshoulder buttons. This API is implemented by simply combining SDL_Joystick\n\twith \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/blob/9772d0512c8a0bb1841244ef9043e598ba0c0ff7/src/joystick/SDL_gamecontrollerdb.h\"\u003ea\n\tlong list of mappings for specific controllers\u003c/a\u003e, and therefore doesn't\n\twork with joypads that don't match this standard.\n\t\u003cfigure\u003e\n\t\t\u003cembed src=\"/blog/static/2023-09-30-SDL_GameController.svg?2c4401cc\"\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\t\u003ca\n\t\t\thref=\"https://raw.githubusercontent.com/libsdl-org/SDL/SDL2/test/controllermap.bmp\"\u003eAccording\n\t\t\tto SDL\u003c/a\u003e, this is what a \"game controller\" looks like. \u003ca\n\t\t\thref=\"https://github.com/AntiMicroX/antimicrox/issues/346#issuecomment-1268499016\"\u003eHere's\n\t\t\tthe source of the SVG.\u003c/a\u003e\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tTo match Shuusou Gyoku's original WinMM backend, we'd ideally want to keep\n\tthe best aspects from both APIs but without being restricted to\n\tSDL_GameController's idea of a controller. The \u003ccode lang=\"ja\"\u003eJoy\n\tPad\u003c/code\u003e menu just identifies each button with a numeric ID, so\n\tSDL_Joystick would be a natural fit. But what do we do about directional\n\tcontrols if SDL_Joystick doesn't tell us which joypad axes correspond to the\n\tX and Y directions, and we don't have the SDL-recommended \u003ca class=\"goal\"\n\thref=\"https://github.com/nmlgc/ssg/issues/51\"\u003econfiguration UI\u003c/a\u003e yet?\n\tDoing that right would also mean \u003ca\n\thref=\"https://github.com/thpatch/thcrap/releases/tag/2018-05-21\"\u003esupporting\n\tPOV hats and D-pads,\u003c/a\u003e after all… Luckily, all joypads we've tested map\n\ttheir main X axis to ID 0 and their main Y axis to ID 1, so this seems like\n\ta reasonable default guess.\n\u003c/p\u003e\u003cp\u003e\n\tFortunately, \u003ca\n\thref=\"https://discourse.libsdl.org/t/difference-between-joysticks-and-game-controllers/24028/2\"\u003ethere\n\tis a solution for our exact issue\u003c/a\u003e. We can still \u003ci\u003etry\u003c/i\u003e to open a\n\tjoypad via SDL_GameController, and if that succeeds, we can use \u003ca\n\thref=\"https://wiki.libsdl.org/SDL_GameControllerGetBindForAxis\"\u003ea function\n\tto retrieve the SDL_Joystick ID for the main X and Y axis\u003c/a\u003e, close the\n\tSDL_GameController instance, and keep using SDL_Joystick for the rest of the\n\tgame.\u003cbr\u003e\n\tAnd with that, the SDL build no longer needs DirectInput 7, \u003ca href=\"https://www.hybrid-analysis.com/sample/e7e5e6206500b69e870d4861984165cddfa32c68c9e197594b4241b85f4fbfa4\"\u003ecertain antivirus scanners will no longer complain about\n\tits low-level keyboard hook\u003c/a\u003e, and I turned the original game's\n\tsingle-joypad hot-plugging into multi-joypad hot-plugging with barely any\n\tcode. 🎮\n\u003c/p\u003e\u003cp\u003e\n\tThe necessary consolidation of the game's original input handling uncovered\n\tseveral minor bugs around the High Score and Game Over screen that I\n\tsufficiently described in the release notes of the new build. But it also\n\trevealed an interesting detail about the \u003ccode lang=\"ja\"\u003eJoy Pad\u003c/code\u003e\n\tscreen: Did you know that Shuusou Gyoku lets you \u003ci\u003eunbind\u003c/i\u003e all these\n\tactions by pressing more than one joypad button at the same time? The\n\toriginal game indicated unbound actions with a \u003ccode lang=\"ja\"\u003e[Button\n\t0]\u003c/code\u003e label, which is pretty confusing if you have ever programmed\n\tanything because you now no longer know whether the game starts numbering\n\tbuttons at 0 or 1. This is now communicated much more clearly.\n\u003c/p\u003e\u003cfigure class=\"fullres\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-09-30-SH01-Joypad-unmapping-original.png?9fe43b80\"\n\t\tdata-title=\"Original game\"\n\t\talt=\"Joypad button unbinding in the original version of Shuusou Gyoku, indicated by a rather confusing [Button 0] label\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-09-30-SH01-Joypad-unmapping-P0256.png?f7adebb7\"\n\t\tdata-title=\"P0256 build\"\n\t\talt=\"Joypad button unbinding in the P0256 build of Shuusou Gyoku, using a much clearer [--------] label\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\u003ccode lang=\"ja\"\u003eESC\u003c/code\u003e is not bound to any joypad button in\n\teither screenshot, but it's only really obvious in the P0256\n\tbuild.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith that, we're finally feature-complete as far as this delivery is\n\tconcerned! Let's send a build over to the backers as a quick sanity check…\n\ta~nd they quickly found a bug when running on Linux and Wine. When holding a\n\tbutton, the game randomly stops registering directional inputs for a short\n\twhile on some joypads? Sounds very much like a Wine bug, especially if the\n\tsame pad works without issues on Windows.\u003cbr\u003e\n\tAnd indeed, on certain joypads, Wine maps the buttons to completely\n\tdifferent and disconnected IDs, as if it simply invents new buttons or axes\n\tto fill the resulting gaps. Until we can \u003ca class=\"goal\"\n\thref=\"https://github.com/nmlgc/ssg/issues/52\"\u003edifferentiate joypad bindings\n\tper controller\u003c/a\u003e, it's therefore unlikely that you can use the same joypad\n\tmapping on both Windows and Linux/Wine without entering the \u003ccode\n\tlang=\"ja\"\u003eJoy Pad\u003c/code\u003e menu and remapping the buttons every time you\n\tswitch operating systems.\n\u003c/p\u003e\u003cp\u003e\n\tStill, by itself, this shouldn't cause any issues with my SDL event handling\n\tcode… except, of course, if I forget a \u003ccode\u003ebreak;\u003c/code\u003e in a switch case.\n\t🫠\u003cbr\u003e\n\tThis completely preventable implicit fallthrough has now caused a few hours\n\tof debugging on my end. I'd better crank up the warning level to keep this\n\tfrom ever happening again. Opting into this specific warning also revealed\n\twhy we haven't been getting it so far: Visual Studio did gain a whole host\n\tof new warnings related to the \u003ca\n\thref=\"https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines\"\u003eC++ Core\n\tGuidelines\u003c/a\u003e a while ago, including \u003ca\n\thref=\"https://learn.microsoft.com/en-us/cpp/code-quality/c26819\"\u003ethe one I\n\twas looking for\u003c/a\u003e, but actually getting the compiler to throw these\n\trequires \u003ca\n\thref=\"https://learn.microsoft.com/en-us/cpp/build/reference/analyze-code-analysis?view=msvc-170\"\u003eactivating\n\ta separate static analysis mode together with a plugin\u003c/a\u003e, which\n\tsignificantly slows down build times. Therefore I only activate them for\n\trelease builds, since these already take long enough. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\u003c/p\u003e\u003cp\u003e\n\tBut that wasn't the only step I took as a result of this blunder. In\n\taddition, I now offer \u003ca href=\"/faq#mod-bugs\"\u003efree fixes for regressions in\n\tmy mod releases if anyone else reports an issue before I find it myself\u003c/a\u003e.\n\tI've already been following this policy\n\t\u003ca href=\"/blog/2023-03-14\"\u003e📝 earlier this year when mu021 reported the unblitting bug in the initial release of the TH01 Anniversary Edition\u003c/a\u003e,\n\tand merely made it official now. If I was the one who broke a thing, I'll\n\tfix it for free.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"screenshots-2023-09-30\"\u003e\n\tSince all that input debugging already started a 5\u003csup\u003eth\u003c/sup\u003e push, I\n\tmight as well fill that one by restoring the original screenshot feature.\n\tAfter all, it's triggered by a key press (and is thus related to the input\n\tbackend), reads the contents of the frame buffer (and is thus related to the\n\tgraphics backend), and it honestly looks bad to have this disclaimer in the\n\trelease notes just because we're one small feature away from 100% parity\n\twith pbg's original binary.\u003cbr\u003e\n\tCoincidentally, I had already written code to save a DirectDraw surface to a\n\t.BMP file for all the debugging I did in the last delivery, so we were\n\t\u003ci\u003ebasically\u003c/i\u003e only missing filename generation. Except that Shuusou\n\tGyoku's original choice of mapping screenshots to the PrintScreen key did\n\tnot age all too well:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAs of Windows XP's 64-bit version, \u003ca\n\thref=\"https://github.com/libsdl-org/SDL/issues/551\"\u003eyou can no longer use\n\tstandard window messages to detect that this key is being pressed\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eAnd as of Windows 11, the OS takes full control of the key by binding it\n\tto the Snipping Tool by default, complete with a UI that politely steals\n\tfocus when hitting that key.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs a result, both \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e and I independently arrived at the\n\tidea of remapping screenshots to the P key, which is the same screenshot key\n\tused by every Windows Touhou game since TH08.\n\u003c/p\u003e\u003cp\u003e\n\tThe rest of the feature remains unchanged from how it was in pbg's original\n\tbuild and will save every distinct frame rendered by the game (i.e., before\n\tflipping the two framebuffers) to a .BMP file as long as the P key is being\n\theld. At a 32-bit color depth, these screenshots take up 1.2\u0026nbsp;MB per\n\tframe, which will quickly add up – especially since you'll probably hold the\n\tP key for more than \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e60\u003c/sub\u003e of a second and therefore end\n\tup saving multiple frames in a row. \u003ca class=\"goal\"\n\thref=\"https://github.com/nmlgc/ssg/issues/54\"\u003eWe should probably compress\n\tthem one day.\u003c/a\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"utmath-2023-09-30\"\u003e\n\tSince I already translated some of Shuusou Gyoku's ASM code to C++ during\n\tthe Zig experiment, it made sense to finish the fifth push by covering the\n\trest of those functions. The integer math functions are used all throughout\n\tthe game logic, and are the main reason why this goal is important for a\n\tLinux port, or any port to a 64-bit architecture for that matter. If you've\n\tever read a \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/micro-optimization\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/a\u003e\u003c/span\u003e-related blog post, you'll know that hand-written ASM is a great recipe that often results in the finest jank, and the game's square root function definitely delivers in that regard, right out of the gate.\u003cbr\u003e\n\tWhat slightly differentiates this algorithm from the typical definition of\n\tan \u003ca href=\"https://en.wikipedia.org/wiki/Integer_square_root\"\u003einteger\n\tsquare root\u003c/a\u003e is that it rounds up: In real numbers, \u003ccode\u003e√﻿3\u003c/code\u003e is\n\t≈\u0026nbsp;1.73, so \u003ccode\u003eisqrt(3)\u003c/code\u003e returns 2 instead of 1. However, if\n\tthe result is always rounded down, you can determine whether you have to\n\tround up by simply squaring the calculated root and comparing it to the \u003ca\n\thref=\"https://en.wiktionary.org/wiki/radicand\"\u003eradicand\u003c/a\u003e. And even that\n\tis only necessary if the difference between the two doesn't naturally fall\n\tout of the algorithm – which is what also happens with Shuusou Gyoku's\n\toriginal ASM code, but \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/DirectXUTYs/UT_MATH.C#L182-L188\"\u003epbg\n\tdidn't realize this and squared the result regardless\u003c/a\u003e. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tThat's one suboptimal detail already. Let's call the original ASM function\n\tin a loop over the entire supported range of radicands from 0 to\n\t2\u003csup\u003e31\u003c/sup\u003e and produce a list of results that I can verify my C++\n\ttranslation against… and watch as the function's linear time complexity with\n\tregard to the radicand causes the loop to run for over \u003ci\u003e15 hours\u003c/i\u003e on my\n\tsystem. 🐌 In a way, I've found the literal opposite of \u003ccode\u003e\u003ca\n\thref=\"https://en.wikipedia.org/wiki/Fast_inverse_square_root\"\u003eQ_rsqrt()\u003c/a\u003e\u003c/code\u003e\n\there: Not fast, not inverse, no bit hacks, and surely without the\n\tawe-inspiring kind of WTF.\u003cbr\u003e\n\tI really didn't want to run the same loop over \u003ca\n\thref=\"https://github.com/nmlgc/ssg/commit/5f06cd419f646eb363db528eda7186a29be2fb73\"\u003ea\n\tliteral C++ translation of the same algorithm\u003c/a\u003e afterward. Calculating\n\tinteger square roots is a common problem with lots of solutions, so let's\n\tsee if we can go better than linear.\n\u003c/p\u003e\u003cp\u003e\n\tAnd indeed, \u003ca\n\thref=\"https://en.wikipedia.org/w/index.php?title=Methods_of_computing_square_roots\u0026oldid=1174851812#Binary_numeral_system_(base_2)\"\u003eWikipedia\n\talso has a bitwise algorithm that runs in logarithmic time\u003c/a\u003e, uses only\n\tadditions, subtractions, and bit shifts, and even ends up with an error term\n\tthat we can use to round up the result as necessary, without a\n\tmultiplication. And this algorithm delivers the exact same results over the\n\texact same range in… 50 seconds. 🏎️ And that's \u003ci\u003ewith\u003c/i\u003e the I/O to print\n\tthe first value that returns each of the 46,341 different square root\n\tresults.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ci\u003e\"But wait a moment!\"\u003c/i\u003e, I hear you say. \u003ci\u003e\"Why are you bothering with\n\tan integer square root algorithm to begin with? Shouldn't good old\n\t\u003ccode\u003eround(sqrt(x))\u003c/code\u003e from \u003ccode\u003e\u0026lt;math.h\u0026gt;\u003c/code\u003e do the trick\n\tjust fine? Our CPUs have had SSE for a long time, and this probably compiles\n\tinto the single \u003ccode\u003eSQRTSD\u003c/code\u003e instruction. All that extra\n\tfloating-point hardware might mean that this instruction could even run in\n\tparallel with non-SSE code!\"\u003c/i\u003e\u003cbr\u003e\n\tAnd yes, all of that is technically true. So I tested it, and my very\n\tsynthetic and constructed micro-benchmark did indeed deliver the same\n\tresults in… 48 seconds. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e That's not enough of a\n\tdifference to justify breaking the spirit of treating the FPU as lava that\n\tpermeates Shuusou Gyoku's code base. Besides, it's not used for that much to\n\tbegin with:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003epre-calculating the 西方Ｐｒｏｊｅｃｔ lens ball effect\u003c/li\u003e\n\t\u003cli\u003ethe fade animation when entering and leaving stages\u003c/li\u003e\n\t\u003cli\u003erendering the circular part of stationary lasers\u003c/li\u003e\n\t\u003cli\u003epulling items to the player when bombing\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAfter a quick C++ translation of the RNG function that spells out a 32-bit\n\tmultiplication on a 32-bit CPU using 16-bit instructions, we reach the final\n\tpieces of ASM code for the 8-bit \u003ccode\u003eatan2()\u003c/code\u003e and trapezoid\n\trendering. These could actually pass for well-written ASM code in how they\n\texpress their 64-bit calculations: \u003ccode\u003eatan8()\u003c/code\u003e prepares its 64-bit\n\tdividend in the combined \u003ccode\u003eEDX\u003c/code\u003e and \u003ccode\u003eEAX\u003c/code\u003e registers in\n\ta way that isn't obvious at all from a cursory look at the code, and the\n\ttrapezoid functions effectively use Q32.32 subpixels. C++ allows us to\n\tcleanly model all these calculations with 64-bit variables, but\n\tunfortunately compiles the divisions into a call to a comparatively much\n\tmore bloated 64-bit/64-bit-division polyfill function. So yeah, we've\n\tactually found a well-optimized piece of inline assembly that even Visual\n\tStudio 2022's optimizer can't compete with. But then again, this is all\n\tabout code generation details that are specific to 32-bit code, and it\n\twouldn't be surprising if that part of the optimizer isn't getting much\n\tattention anymore. Whether that optimization was useful, on the other hand…\n\tOh well, the new C++ version will be much more efficient in 64-bit builds.\n\u003c/p\u003e\u003cp\u003e\n\tAnd with that, there's no more ASM code left in Shuusou Gyoku's codebase,\n\tand the original \u003ccode\u003eDirectXUTYs\u003c/code\u003e directory is slowly getting\n\temptier and emptier.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tPhew! Was that everything for this delivery? I think that was everything.\n\tHere's the new build, which checks off 7 of the 15 remaining portability\n\tboxes:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0256\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0256\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Taking a well-earned break from Shuusou Gyoku and starting with the\n\tpreparations for multilingual PC-98 Touhou translatability by looking at\n\tTH04's and TH05's in-game dialog system, and definitely writing a shorter\n\tblog post about all that…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-11-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-08-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-09-30T21:01:36Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-08-01",
      "url": "https://rec98.nmlgc.net/blog/2023-08-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-09-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-07-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-08-01\"\u003e\u003ctime datetime=\"2023-08-01T23:57:43Z\"\u003e2023-08-01 23:57\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0246\"\u003eP0246\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Cleanup and deduplication)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/P0226...152ad74\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0247\"\u003eP0247\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Graphics refactoring, part 1/5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/152ad74...54c3c4e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0248\"\u003eP0248\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (秋霜CFG.DAT versioning)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/54c3c4e...62ff407\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0249\"\u003eP0249\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Graphics refactoring, part 2/5 + Coordinate systems, part 1/?)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/62ff407...a1f80a3\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0250\"\u003eP0250\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Text pre-rendering, part 1/2: Implementation + Migrating the in-game music title)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/a1f80a3...629ddd8\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0251\"\u003eP0251\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Text pre-rendering, part 2/2: Migrating the rest of the game + Replacing Win32 memory management)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/629ddd8...P0251\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e, alp-bib\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAnd then I'm even late by yet another two days… For some reason, preparing\n\tShuusou Gyoku for an OpenGL port has been the most difficult and drawn-out\n\ttask I've worked on so far throughout this project. These pushes were in\n\tdevelopment since April, and over two months in total. Tackling a legacy\n\tcodebase with such a rather vague goal while simultaneously wanting to keep\n\teverything running did not do me any favors, and it was pretty hard to\n\tresist the urge to fix \u003ci\u003eeverything\u003c/i\u003e that had better be fixed to make\n\tthis game portable…\u003cbr\u003e\n\t\u003ca href=\"/blog/2022-12-31\"\u003e📝 2022 ended with Shuusou Gyoku working at full speed on Windows ≥8 by itself\u003c/a\u003e, without external tools, for the first\n\ttime. However, since it all came down to just one small bugfix, the\n\tresulting build still had several issues:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe game might still start in the slow, \u003cq\u003emitigated\u003c/q\u003e 8-bit or 16-bit\n\tmode if the respective app compatibility flag is still present in the\n\tregistry from the earlier \u003ca href=\"/blog/2022-09-04\"\u003e📝 P0217 build\u003c/a\u003e. A\n\tplayer would then have to manually put the game into 32-bit mode via the\n\tOption menu to make it run at its actual intended speed. Bypassing this flag\n\tprogrammatically would require some rather fiddly .EXE patching techniques.\n\t(\u003ca href=\"https://github.com/nmlgc/ssg/issues/33\"\u003e#33\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eThe 32-bit mode tends to lag significantly if a lot of sprites are\n\tonscreen, for example when canceling the final pattern of the Extra Stage\n\tmidboss. (\u003ca href=\"https://github.com/nmlgc/ssg/issues/35\"\u003e#35\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eIf the game window lost and regained focus during the ending (for\n\texample via Alt-Tabbing), the game reloads the wrong sprite sheet. (\u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/19\"\u003e#19\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eAnd, of course, we still have no native windowed mode, or support for\n\trendering in the higher resolutions you'd want to use on modern high-DPI\n\tdisplays. (\u003ca href=\"https://github.com/nmlgc/ssg/issues/7\"\u003e#7\u003c/a\u003e)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNow, we could tackle all of these issues one by one, in focused pushes… or\n\twait for one hero to fund a full-on OpenGL backend as part of the larger\n\tgoal of porting this game to Linux. This would take much longer, but fix all\n\tthese issues at once while bringing us significantly closer to Shuusou Gyoku\n\tbeing cross-platform. Which is exactly what Ember2528 did.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#strategy-2023-08-01\"\u003eThe migration strategy\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#ddraw-2023-08-01\"\u003eLearning DirectDraw and its surface management\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#palettes-2023-08-01\"\u003eReverse-engineering the palette logic in 8-bit mode\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#text-2023-08-01\"\u003ePortable, offscreen text rendering\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#config-2023-08-01\"\u003eThe new, forward-compatible configuration file format\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#sdl-2023-08-01\"\u003eSDL_Renderer as the perfect abstraction?\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"strategy-2023-08-01\"\u003e\u003cp\u003e\n\tShuusou Gyoku is a \u003ci\u003every\u003c/i\u003e Windows-native codebase. Its usage of types\n\tdeclared in \u003ccode\u003e\u0026lt;windows.h\u0026gt;\u003c/code\u003e even extends to core gameplay\n\tcode, the rendering code is completely architected around DirectDraw's\n\tfeatures and drawbacks, and text rendering is not abstracted at all. Looks\n\tlike it's now my task to write all the abstractions that pbg didn't manage\n\tto write…\u003cbr\u003e\n\tTherefore, I chose to stay with DirectDraw for a few more pushes while I\n\twould build these abstractions. In hindsight, this was the least efficient\n\tapproach one could possibly imagine for the exact goal of porting the game\n\tto Linux. Suddenly, I had to \u003ci\u003eunderstand\u003c/i\u003e all this  DirectDraw and GDI\n\tjank, just to keep the game running at every step along the way. Retaining\n\tShuusou Gyoku's 8-bit mode in particular was a huge pain, but I didn't want\n\tto remove it because it's currently the only way I can easily debug the game\n\tin windowed mode at a scaled resolution, through \u003ca\n\thref=\"https://sourceforge.net/projects/dxwnd/\"\u003eDxWnd\u003c/a\u003e. In 16-bit or\n\t32-bit mode, DxWnd slows down to a crawl, roughly resembling the performance\n\tdrop we used to get with Windows' own compatibility mitigations for the\n\toriginal build.\u003cbr\u003e\n\tThe upside, though, is that everything I've built so far still works with\n\tthe original 8-bit and 16-bit graphics modes. And with just \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/43\"\u003eone compiler flag to disable\n\tany modern x86 instructions\u003c/a\u003e, my build can still run on \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Pentium_(original)\"\u003ei586/P5 Pentium\n\tCPUs\u003c/a\u003e, and only requires \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/43\"\u003eKernelEx and its latest\n\tKstub822 patches\u003c/a\u003e to run on Windows 98. And, surprisingly, \u003ca\n\thref=\"https://twitter.com/Columbio184/status/1685823332300562433\"\u003emy core\n\taudience does appreciate this fact\u003c/a\u003e. Thus, I will include an i586 build\n\tin all of my upcoming Shuusou Gyoku releases from now on. Once this codebase\n\tcan compile into a 64-bit binary (which will obviously be required for a\n\tnative Linux build), the i586 build will remain the only 32-bit Windows\n\tbuild I'll include in my releases.\n\u003c/p\u003e\u003chr id=\"ddraw-2023-08-01\"\u003e\u003cp\u003e\n\tSo, what was DirectDraw? In the shortest way that still describes it\n\taccurately from the point of view of a developer: \"A hardware acceleration\n\tlayer over Ye Olde Win32 GDI, providing double-buffering and fast blitting\n\tof rectangles.\" There's the \u003ci\u003eprimary\u003c/i\u003e double-buffered framebuffer\n\tsurface, the \u003cdfn\u003eoffscreen surfaces\u003c/dfn\u003e that you create (which are\n\tcomparable to what 3D rendering APIs would call \u003ci\u003etextures\u003c/i\u003e), and you\n\tcan blit rectangular regions between the two. That's it. Except for\n\tdouble-buffering, DirectDraw offers no feature that GDI wouldn't also\n\tsupport, while not covering some of GDI's more complex features. I mean,\n\tDirectDraw can blit rectangles only? \u003ca\n\thref=\"https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-plgblt\"\u003eHow\n\tlame.\u003c/a\u003e \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tHowever, DirectDraw's relative lack of features is not as much of a problem\n\tas it might appear at first. The reason for that lies in what I consider to\n\tbe DirectDraw's actual killer feature: compatibility with GDI's \u003cdfn\u003edevice\n\tcontext\u003c/dfn\u003e (DC) abstraction. By acquiring a DC for a DirectDraw surface,\n\tyou can use all existing GDI functions to draw onto the surface, and, in\n\tgeneral, it will all just work. 😮 Most notably, you can use GDI's blitting\n\tfunctions (i.e., \u003ccode\u003eBitBlt()\u003c/code\u003e and friends) to transfer pixel data\n\tfrom a GDI \u003ccode\u003eHBITMAP\u003c/code\u003e in system memory onto a DirectDraw surface\n\tin video memory, which is the easiest and most straightforward way to, well,\n\t\u003ci\u003eget sprite data onto a DirectDraw surface in the first place\u003c/i\u003e.\u003cbr\u003e\n\tIn theory, you could do that without ever touching GDI by locking the\n\tsurface memory and writing the raw bytes yourself. But in practice, you\n\tprobably won't, because your game has to run under multiple bit depths and\n\tyour data files typically only store one copy of all your sprites in a\n\tsingle bit depth. And the necessary conversion and palette color matching…\n\tis a mere implementation detail of GDI's blitting functions, using a\n\tsupposedly optimized code path for every permutation of source and\n\tdestination bit depths.\n\u003c/p\u003e\u003cp\u003e\n\tAll in all, DirectDraw doesn't look too bad so far, does it? Fast blitting,\n\tand you can still use the full wealth of GDI functions whenever needed… at\n\tthe small cost of potentially losing your surface memory at any time. 🙄\n\tYup, if a DirectDraw game runs in true resolution-changing fullscreen mode\n\tand you switch to the Windows desktop, all your surface memory is freed and\n\tyou have to manually restore it once the game regains focus, followed by\n\tmanually copying all intended bitmap data back onto all surfaces. DirectDraw\n\tis where this concept of surface loss originated, which later carried over\n\tto the earlier versions of Direct3D and, \u003ca\n\thref=\"https://web.archive.org/web/20110708090748/http://braid-game.com/news/2009/01/the-jeff-and-casey-show-on-visual-studio-2010-and-direct2d/\"\u003einfamously,\n\tDirect2D\u003c/a\u003e as well.\u003cbr\u003e\n\tLooking at it from the point of view of the mid-90s, it does make sense to\n\tlet the application handle trashed video memory if that's an unfortunate\n\treality that your graphics API implementation has to deal with. You don't\n\twant to retain a second copy of each surface in a less volatile part of\n\tmemory because you didn't have that much of it. Instead, the application can\n\tnow choose the most appropriate way to restore each individual surface. For\n\tprocedurally generated surfaces, it could just re-run the generating code,\n\twhereas all the fixed sprite sheets could be reloaded from disk.\n\u003c/p\u003e\u003cp\u003e\n\tIn practice though, this well-intentioned freedom turns into a huge pain.\n\tSuddenly, it's no longer enough to load every sprite sheet once before it's\n\tneeded, blit its pixel data onto the DirectDraw surface, and forget about\n\tit. Now, the renderer must also be able to refresh the pixel data of every\n\tsurface \u003ci\u003efrom within itself\u003c/i\u003e whenever any of DirectDraw's blitting\n\tfunctions fails with a \u003ccode\u003eDDERR_SURFACELOST\u003c/code\u003e error. This fact alone\n\tis enough to push your renderer interface towards central management and\n\tallocation of surfaces. You could \u003ci\u003emaybe\u003c/i\u003e avoid the conceptual\n\t\u003ccode\u003eSurfaceManager\u003c/code\u003e by bundling each surface with a regeneration\n\tcallback, but why should you? Any other graphics API would work with\n\tstraight-line procedural load-and-forget initialization code, so why slice\n\tthat code into little parts just because of some DirectDraw quirk?\n\u003c/p\u003e\u003cp\u003e\n\tSo if your surfaces can get trashed at any time, \u003ci\u003eand\u003c/i\u003e you already use\n\tGDI to copy them from system memory to DirectDraw-managed video memory,\n\t\u003ci\u003eand\u003c/i\u003e your game features at least one procedurally generated surface…\n\tyou might as well retain every currently loaded surface in the form of an\n\tadditional GDI device-independent bitmap. 🤷 In fact, that's even better\n\tthan what Shuusou Gyoku did originally: For all .BMP-sourced surfaces, it\n\tonly kept a buffer of the entire decompressed .BMP file data, which means\n\tthat it had to recreate said intermediate GDI bitmap every time it needed to\n\trestore a surface. The in-game music title \u003ci\u003ewas\u003c/i\u003e originally restored\n\tvia regeneration callback that re-rendered the intended title directly onto\n\tthe DirectDraw surface, but this was handled by an additional \"restore hook\"\n\tsystem that remained unused for anything else.\u003cbr\u003e\n\tAnything more involved would be a micro-optimization, especially since the\n\tgoal is to get \u003ci\u003eaway\u003c/i\u003e from DirectDraw here. Not much point in \"neatly\"\n\treloading sprite surfaces from disk if the total size of all loaded sprite\n\tsheets barely exceeds the 1 MiB mark. Also, keeping these GDI DIBs loaded\n\tand initialized \u003ci\u003edoes\u003c/i\u003e speed up getting back into the game… in theory,\n\tat least. After all, the game still runs in fullscreen mode, and resolution\n\tswitching already takes longer on modern flat-panel displays than any\n\tsurface restoration method we could come up with.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr id=\"palettes-2023-08-01\"\u003e\u003cp\u003e\n\tSo that was all pretty annoying. But once we start rendering in 8-bit mode,\n\tit gets even worse as we suddenly have to bother with palette management.\n\tSimilar to \u003ca href=\"/blog/tag/th01/palette\"\u003ePC-98 Touhou\u003c/a\u003e, Shuusou Gyoku\n\tuses \u003ci\u003eway\u003c/i\u003e too many different palettes. In fact, it \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/e6267e99dfaac467bf42d0336c3675c74c2441af/DirectXUTYs/DD_UTY.CPP#L694-L705\"\u003ecreates\n\ta separate DirectDraw palette to retain the palette embedded into every\n\tloaded .BMP file, and simply sets the palette of the primary surface and the\n\tbackbuffer to the one it loaded last\u003c/a\u003e. Like, why would you retain\n\tper-surface palettes, and what effect does this even have? What even happens\n\twhen you blit between two DirectDraw surfaces that have different palettes?\n\tMight this be the cause of the discolored in-game music title when playing\n\tunder DxWnd? 😵\u003cbr\u003e But if we try throwing out those extra palettes, it\n\tonly takes until Stage 3 for us to be greeted with… the infamous golf\n\tcourse:\n\u003c/p\u003e\u003cfigure class=\"singleplayer_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-Gates-with-single-palette.png?c3a9641c\" alt=\"Shuusou Gyoku's Stage 3 if it only used the palette it loaded last\"\u003e\u003cfigcaption\u003e\n\t\tLooks familiar? You might remember these colors from your attempts to \u003ca\n\t\thref=\"https://en.touhouwiki.net/index.php?title=Shuusou_Gyoku/Gameplay\u0026diff=prev\u0026oldid=336642#Running_on_the_latest_computers\"\u003erun\n\t\tthe original build using D3DWindower\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp style=\"\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\talign-items: center;\n\tgap: 0.5em;\n\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-Gates-face.png?0c8bea4f\"\u003e\u003cspan\u003e\n\tAs you might have guessed, these exact colors come from Gates' face sprite,\n\twhose palette apparently doesn't match the sprite sheets used in Stage 3.\n\tTurns out that 256 colors are not enough for what Shuusou Gyoku would like\n\tto use across the entire stage. In sprite loading order:\u003c/span\u003e\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"numbers\"\u003e\n\t\u003cthead\u003e\u003ctr\u003e\n\t\t\u003cth\u003eSprite sheet\u003c/th\u003e\n\t\t\u003cth\u003e\u003ccode\u003eGRAPH.DAT\u003c/code\u003e file\u003c/th\u003e\n\t\t\u003cth\u003eAdditional unique colors\u003c/th\u003e\n\t\t\u003cth\u003eTotal unique colors\u003c/th\u003e\n\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eGeneral system sprites\u003c/td\u003e\n\t\t\u003ctd\u003e#0\u003c/td\u003e\n\t\t\u003ctd\u003e+96\u003c/td\u003e\n\t\t\u003ctd\u003e96\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eStage 3 enemies\u003c/td\u003e\n\t\t\u003ctd\u003e#3\u003c/td\u003e\n\t\t\u003ctd\u003e+42\u003c/td\u003e\n\t\t\u003ctd\u003e138\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eStage 3 map tiles\u003c/td\u003e\n\t\t\u003ctd\u003e#9\u003c/td\u003e\n\t\t\u003ctd\u003e+40\u003c/td\u003e\n\t\t\u003ctd\u003e178\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eWide Shot bomb cut-in\u003c/td\u003e\n\t\t\u003ctd\u003e#26\u003c/td\u003e\n\t\t\u003ctd\u003e+3\u003c/td\u003e\n\t\t\u003ctd\u003e181\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eVIVIT's faceset\u003c/td\u003e\n\t\t\u003ctd\u003e#13\u003c/td\u003e\n\t\t\u003ctd\u003e+40\u003c/td\u003e\n\t\t\u003ctd\u003e221\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eUnknown face\u003c/td\u003e\n\t\t\u003ctd\u003e#14\u003c/td\u003e\n\t\t\u003ctd\u003e+35\u003c/td\u003e\n\t\t\u003ctd\u003e256\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tbody\u003e\u003ctfoot\u003e\u003ctr\u003e\n\t\t\u003ctd\u003eGates' faceset\u003c/td\u003e\n\t\t\u003ctd\u003e#17\u003c/td\u003e\n\t\t\u003ctd\u003e+40\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cstrong\u003e296\u003c/strong\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003c/tfoot\u003e\n\u003c/table\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that's why Shuusou Gyoku does not only have to retain these palettes,\n\tbut also contains \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/17894644715b372468f73b52eea1078e93b03344/GIAN07/SCROLL.CPP#L361-L369\"\u003estage\n\tscript commands\u003c/a\u003e (!) to switch the current palette back to either the map\n\tor enemy one, after the dialog system enforced the face palette.\n\u003c/p\u003e\u003cp\u003e\n\tBut the worst aspects about palettes rear their ugly head at the boundary\n\tbetween GDI and DirectDraw, when GDI adds its own palettes into the mix.\n\tNone of the following points are clearly documented in either ancient or\n\tcurrent MSDN, forcing each new DirectDraw developer to figure them out on\n\ttheir own:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhen calling \u003ccode\u003eIDirectDraw::CreateSurface()\u003c/code\u003e in 8-bit mode,\n\tDirectDraw automatically sets up the newly created surface with a reference\n\t(not a copy!) to the palette that's currently assigned to the primary\n\tsurface.\u003c/li\u003e\n\t\u003cli\u003eWhen locking an 8-bit surface for GDI blitting via\n\t\u003ccode\u003eIDirectDrawSurface::GetDC()\u003c/code\u003e, DirectDraw is supposed to set the\n\tGDI palette of the returned DC to the current palette of the DirectDraw…\n\t\u003ci\u003eprimary surface\u003c/i\u003e?! Not the surface you're actually calling\n\t\u003ccode\u003eGetDC()\u003c/code\u003e on?!\u003cbr\u003e\n\tInterestingly, it took until March of this year for DxWnd to \u003ca\n\thref=\"https://github.com/narzoul/DDrawCompat/issues/219\"\u003ediscover a\n\tdifferent game that relied on this detail\u003c/a\u003e, while DDrawCompat had\n\timplemented it for years. DxWnd version 2.05.95 then introduced the\n\t\u003ci\u003eDirectX(2) → Fix DC palette\u003c/i\u003e tweak, and it's this option that would\n\tfix the colors of the in-game music title on any Shuusou Gyoku build older\n\tthan P0251.\u003c/li\u003e\n\t\u003cli\u003eMake sure to \u003ci\u003enever\u003c/i\u003e \u003ccode\u003eBitBlt()\u003c/code\u003e from a 24-bit RGB GDI\n\timage to a palettized 8-bit DirectDraw offscreen surface. You might be\n\ttempted to just go 24-bit because there's no palette to worry about and you\n\tcan retain a single GDI image for every supported bit depth, but the\n\tresulting palette mapping glitches will be much worse than if you just\n\tstayed in 8-bit. If you want to procedurally generate a GDI bitmap for a\n\tDirectDraw surface, for example if you need to render text, just \u003ca\n\thref=\"https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createcompatiblebitmap\"\u003ecreate\n\ta bitmap that's \u003ci\u003ecompatible\u003c/i\u003e with the DC of DirectDraw's primary or\n\tbackbuffer surface\u003c/a\u003e. Doing that magically removes all palette woes, and\n\t\u003ccode\u003eCreateCompatibleBitmap()\u003c/code\u003e is much easier to call anyway.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUltimately, all of this is why Shuusou Gyoku's original DirectDraw backend\n\tlooks the way it does. It might seem redundant and inefficient in places,\n\tbut pbg did in fact discover the only way where all the undocumented GDI and\n\tDirectDraw color mapping internals come together to make the game look as\n\tintended. 🧑‍🔬\u003cbr\u003e\n\tAnd what else are you going to do if you want to target old hardware? My\n\tPC-9821Nw133, for example, can only run the original Shuusou Gyoku in 8-bit\n\tmode. For a Windows game on such old hardware, 8-bit DirectDraw looks like\n\tthe only viable option. You certainly don't want to use GDI alone, because\n\tthat's probably slow and you'd have to worry about \u003ca\n\thref=\"https://www.compuphase.com/palette.htm\"\u003eeven more palette-related\n\tissues\u003c/a\u003e. Although people have reported that Shuusou Gyoku does actually\n\trun faster on their old Windows 9x machine if they \u003ci\u003edisable\u003c/i\u003e DirectDraw\n\tacceleration…?\u003cbr\u003e\n\tIn that case, it might be worth a try to write a completely new 8-bit\n\tsoftware renderer, employing the same retained VRAM techniques that the\n\tPC-98 Touhou games used to implement their scrolling playfields with a\n\tminimum of redraws. The hardware scrolling feature of the PC-98 GDC would\n\tthen be replicated by blitting the playfield in two halves every frame. I\n\twonder how fast that would be…\u003cbr\u003e\n\tOr you go straight back to DOS, and bring your own font renderer and\n\tMIDI/PCM sound driver. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr id=\"text-2023-08-01\"\u003e\u003cp\u003e\n\tSo why did we have to learn about all this? Well, if GDI functions can\n\tdirectly render onto any kind of DirectDraw surface, this also includes text\n\trendering functions like \u003ccode\u003eTextOut()\u003c/code\u003e and \u003ccode\u003eDrawText()\u003c/code\u003e.\n\tIf you're \u003ci\u003ereally\u003c/i\u003e lazy, you can even render your text directly onto\n\tthe DirectDraw backbuffer, which \u003ci\u003eprobably\u003c/i\u003e re-rasterizes all glyphs\n\tevery frame!\u003cbr\u003e\n\tWhich, you guessed it, is exactly how Shuusou Gyoku renders most of its\n\ttext. 🐷 Granted, it's not too bad with MS Gothic thanks to its embedded\n\tbitmaps for \u003ca\n\thref=\"https://web.archive.org/web/20160402134225/http://tomtia.plala.jp/pc/ttfont/#step04\"\u003efont\n\theights between 7 and 22 inclusive\u003c/a\u003e, which replace the usual Bézier curve\n\trasterization for TrueType fonts with a rather quick bitmap lookup. However,\n\tit would not only become a hypothetical problem if future translations end\n\tup choosing more complex fonts without embedded bitmaps, but also as soon as\n\twe port the game to other systems. Nobody in their right mind would\n\tintegrate a cross-platform font renderer directly with a 3D graphics API… \u003ca\n\thref=\"https://learn.microsoft.com/en-us/windows/win32/opengl/font-and-text-functions\"\u003e\u003cspan\n\tclass=\"hovertext\" title=\"I haven't tried this one for obvious non-portability reasons, but having to specify a *range* of glyphs already means that it's going to be a disaster for Japanese text.\"\u003eright?\u003c/span\u003e\u003c/a\u003e\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tInstead, let's refactor the game to render all its existing text to and from\n\ta \u003cspan class=\"hovertext\" title=\"Remember, we have to keep all images as both GDI bitmaps and DirectDraw surfaces.\"\u003ebitmap\u003c/span\u003e,\n\textending the way the in-game music title is rendered to the rest of the\n\tgame. Conceptually, this is also how the Windows Touhou games have always\n\trendered their text. Since they've always used Direct3D, they've always had\n\tto blit GDI's output onto a texture. Through the definitions in\n\t\u003ccode\u003etext.anm\u003c/code\u003e, this fixed-size texture is then turned into a sprite\n\tsheet, allowing every rendered line of text to be individually placed on the\n\tscreen and animated.\u003cbr\u003e\n\tHowever, the static nature of both the sprite sheet and the texture caused\n\tits fair share of problems for thcrap's translation support. Some of the\n\tsprites, particularly the ones for spell card titles, don't originally take\n\tup the entire width of the playfield, cutting off translations long before\n\tthey reach the left edge. Consequently, thcrap's \u003ca\n\thref=\"https://github.com/thpatch/thcrap-tsa/tree/master/base_tsa\"\u003ebase patch\n\tfor the Windows Touhou games\u003c/a\u003e has to resize the respective sprites to\n\tmake translators happy. Before I added \u003ca\n\thref=\"https://github.com/thpatch/thcrap/releases/tag/2018-10-22\"\u003e.ANM header\n\tpatching in late 2018\u003c/a\u003e, this had to be done through a complete modified\n\tcopy of \u003ccode\u003etext.anm\u003c/code\u003e for every game – with possibly additional\n\tvariants if ZUN changed the layout of this file between game versions. Not\n\tto mention that it's bound to be quite annoying to manually allocate a\n\trectangle for every line of text we want to show. After all, I have \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/36\"\u003eat least\u003c/a\u003e \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/12\"\u003etwo\u003c/a\u003e text-heavy future\n\tfeatures in mind already…\n\u003c/p\u003e\u003cp\u003e\n\tSo let's not do \u003ci\u003eexactly\u003c/i\u003e that. Since DirectDraw wants us to manage all\n\tsurfaces in a central place, we keep the idea of using a single surface for\n\tall text. But instead of predefining anything about the surface layout, we\n\tfully build up the surface at runtime based on whatever rectangles we need,\n\tusing a \u003ca href=\"https://github.com/TeamHypersomnia/rectpack2D\"\u003erectangle\n\tpacking\u003c/a\u003e algorithm… yup, I wouldn't have expected to enter such territory\n\teither. For now, we still hardcode a fixed size that each piece of text is\n\tallowed to maximally take up. But once we get translations, nothing is\n\tstopping us from dynamically extending this size to fit even longer strings,\n\tand fitting them onto the fixed screen space via smooth scrolling.\u003cbr\u003e\n\tTo prevent the surface from arbitrarily growing as the game wants to render\n\tmore and more text, we also reset all allocated rectangles whenever the game\n\tstate changes. In turn, this will also recreate the text surface to match\n\tthe new bounding box of all rectangles before the first prerendering call\n\twith the new layout. And if you remember the first bullet point about\n\tDirectDraw palettes in 8-bit mode, this also means that the text surface\n\tautomatically receives the current  palette of the primary surface, giving\n\tus correct colors even without requiring DxWnd's DC palette tweak. 🎨\n\u003c/p\u003e\u003cp\u003e\n\tIn fact, the need to dynamically create surfaces at custom sizes was the\n\tmain reason why I had to look into DirectDraw surface management to begin\n\twith. The original game \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/7dcab4f00881e7d9211b3f9d4229a78fe9a509e9/GIAN07/LOADER.CPP#L46-L84\"\u003ecreated\n\tall of its surfaces at once\u003c/a\u003e, at startup or after changing the bit depth\n\tin the main menu, which was a bad idea for many reasons:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt hardcoded and limited the size of all sprite sheets,\u003c/li\u003e\n\t\u003cli\u003eadded another rendering-API-specific function that game code should not\n\tneed to worry about,\u003c/li\u003e\n\t\u003cli\u003eintroduced surface \u003ci\u003eIDs\u003c/i\u003e that have to be synchronized with the\n\tsurface \u003ci\u003epointers\u003c/i\u003e used throughout the rest of the game,\u003c/li\u003e\n\t\u003cli\u003eand was the main reason why the game had to distribute the six 320×240\n\tending pictures across two of the fixed 640×480 surfaces, which ended up\n\tcausing the \u003ca href=\"https://github.com/nmlgc/ssg/issues/19\"\u003esprite reload\n\tbug in the ending\u003c/a\u003e. As implied in the issue, this was a DirectDraw bug\n\tthat pretty much had to fix itself before I could port the game to OpenGL,\n\tand was the only bug where this was the case. Check the issue comments for\n\tmore details about this specific bug.\n\u003c/ul\u003e\u003cp\u003e\n\tIn the end, we get four different layouts for the text surface: One for the\n\tmain menu, the Music Room, the in-game portion, and the ending. With,\n\tperhaps surprisingly, not too much text on either of them:\n\u003c/p\u003e\u003cfigure\u003e\u003cfigure class=\"side_by_side small\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-rectangle-packing-Menu.png?a7776fcd\" alt=\"The font-rendered text from Shuusou Gyoku's sound option menu, packed into a texture.\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-rectangle-packing-Music.png?9a5fb0e6\" alt=\"The font-rendered text from Shuusou Gyoku's Music Room, packed into a texture.\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-rectangle-packing-Ingame.png?879ae861\" alt=\"The font-rendered text from Shuusou Gyoku's Stage 1, packed into a texture.\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-rectangle-packing-Ending.png?f9a1e71d\" alt=\"The font-rendered text from Shuusou Gyoku's ending, packed into a texture.\"\u003e\n\u003c/figure\u003e\u003cfigcaption\u003e\n\tYes, the ending uses just a single rectangle that takes up the entire screen\n\tspace below the pictures and credits.\u003cbr\u003e\n\tFor the menus, the resulting packed layout reveals how I'm assigning a\n\tseparately cached rectangle to each possible option – otherwise, they\n\tcouldn't be arranged vertically on screen with this bitmap layout. Right\n\tnow, I'm only storing all text for the current menu level, which requires\n\ttext to be rendered again when entering or leaving submenus. However, I'm\n\tallocating as many rectangles as required for the submenu with the most\n\tamount of items to at least prevent the single text surface from being\n\tresized while navigating through the menu. As a side effect, this is also\n\twhy you can see multiple \u003ccode\u003eExit\u003c/code\u003e labels: These simply come from\n\tother submenus with more elements than the currently visited \u003ccode\u003eSound /\n\tMusic\u003c/code\u003e one.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tStill, we're re-rasterizing whole lines of text exactly as they appear on\n\tscreen, and are even doing so multiple times to apply any drop shadows.\n\tIsn't that exactly what every text rendering tutorial nowadays advises\n\tagainst doing? Why not directly go for the classic solution to this problem\n\tand render using a \u003ca\n\thref=\"https://straypixels.net/texture-packing-for-fonts/\"\u003efont texture\n\tatlas\u003c/a\u003e? Well…\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eMost of the game text is still in Japanese. If we were to build a font\n\tatlas in advance, we'd have to add a separate build step that collects all\n\tneeded codepoints by parsing all text the game would ever print, adding a\n\tbuild-time dependency on the original game's copyrighted data files. We'd\n\talso have to move all hardcoded strings to a separate file since we surely\n\tdon't want to parse C++ manually during said build step. Theoretically, we\n\twould then also give up the idea of modding text at run-time without\n\tre-running that build step, since we'd restrict all text to the glyphs we've\n\trasterized in the atlas… yeah, that's more than enough reasons for static\n\tatlas generation to be a non-starter.\u003cbr\u003e\n\tOK, then let's build the atlas dynamically, adding new glyphs as we\n\tencounter them. Since this game is old, we can even be a bit lazy as far as\n\tthe packing is concerned, and don't have to get as fancy as the GIF in the\n\tlink above. Just assume a fixed height for each glyph, and fill the atlas\n\tfrom left to right. We can even clear it periodically to keep it from\n\tgetting too big, like before entering the Music Room, the in-game portion,\n\tor the ending, or after switching languages once we have translations.\n\tShould work, right?\u003c/li\u003e\n\t\u003cli\u003eExcept that most text in Shuusou Gyoku comes with a shadow, realized by\n\tfirst drawing the same string in a darker color and displaced by a few\n\tpixels. With a 3D renderer, none of this would be an issue because we can\n\tdefine vertex colors. But we're still using DirectDraw, which has no way of\n\tapplying any sort of color formula – again, all it can do is take a\n\trectangle and blit it somewhere else. So we can't just keep one atlas with\n\twhite glyphs and let the renderer recolor it. Extending Shuusou Gyoku's\n\tDirect3D code with support for textured quads is also out of the question\n\tbecause then we wouldn't have any text in the Direct3D-less 8-bit mode. So\n\twhat do we do instead? Throw the atlas away on every color change? Keep\n\tmultiple atlases for every color we've seen so far? Turn shadows into a\n\thigh-level concept? Outright forgetting the idea seems to be the best choice\n\there…\u003c/li\u003e\n\t\u003cli\u003eFor a rather square language like Japanese where one Shift-JIS codepoint\n\talways corresponds to one glyph, a texture atlas can work fine and without\n\ttoo much effort. But once we support languages with more complex ligatures,\n\twe suddenly need to get a \u003ca\n\thref=\"https://harfbuzz.github.io/why-do-i-need-a-shaping-engine.html\"\u003eshaping\n\tengine\u003c/a\u003e from somewhere, and directly interact with it from our rendering\n\tcode. This necessarily involves changing APIs and maybe even bundling the\n\tfirst cross-platform libraries, which I wanted to avoid in an already packed\n\tand long overdue delivery such as this one. If we continue to render\n\tline-by-line, translations would only need a line break algorithm.\u003c/li\u003e\n\t\u003cli\u003eMost importantly though: \u003ci\u003eIt's not going to matter anyway.\u003c/i\u003e The\n\tgame ran fine on early 2000s hardware even though it called\n\t\u003ccode\u003eTextOut()\u003c/code\u003e every frame, and any approach that caches the result\n\tof this call is going to be faster.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWhile the Music Room and the ending can be easily migrated to a prerendering\n\tsystem, it's much harder for the main menu. Technically, \u003ci\u003eall\u003c/i\u003e option\n\tstrings of the currently active submenu are rewritten every frame, even\n\tthough that would only be necessary for the scrolling MIDI device name in\n\tthe \u003ccode\u003eSound / Music\u003c/code\u003e submenu. And since all this rewriting is done\n\tvia a classic \u003ccode\u003esprintf()\u003c/code\u003e on fixed-size \u003ccode\u003echar\u003c/code\u003e\n\tbuffers, we'd have to deploy our own change detection before prerendering\n\tcan have any performance difference.\u003cbr\u003e\n\tIn essence, we'd be shifting the text rendering paradigm from the original\n\timmediate approach to a more retained one. If you've ever used any of the\n\thot new immediate-mode GUI or web frameworks that have become popular over\n\tthe last 10 years, your alarm bells are probably already ringing by now.\n\tAdding retained elements is always a step back in terms of code quality, as\n\tit increases complexity by storing UI state in a second place.\n\u003c/p\u003e\u003cp\u003e\n\tWouldn't it be better if we could just stay with the original immediate\n\tapproach then? Absolutely, and we only need a simple cache system to get\n\tthere. By remembering the string that was last rendered to every registered\n\trectangle, the text renderer can offer an immediate API that combines the\n\tdistinct \u003ccode\u003ePrerender()\u003c/code\u003e and \u003ccode\u003eBlit()\u003c/code\u003e steps into a\n\tsingle \u003ccode\u003eRender()\u003c/code\u003e call. There still has to be an initialization\n\tpoint that registers all rectangles for each game state (which,\n\tsurprisingly, was not present for the in-game portion in the original code),\n\tbut the rendering code remains architecturally unchanged in how we call the\n\ttext renderer every frame. As long as the text doesn't change, the text\n\trenderer just blits whatever it previously rendered to the respective\n\trectangle. With an API like this, the whole pre-rendering part turns into a\n\tmere implementation detail.\n\u003c/p\u003e\u003cp\u003e\n\tSo, how much faster is the result? Since I can only measure non-VSynced\n\tperformance in a quite rudimentary way using DxWnd's FPS counter, it highly\n\tdepends on the selected renderer. Weirdly enough, even just switching font\n\t\u003ci\u003ecreation\u003c/i\u003e to the Unicode APIs tripled the FPS inside the Music Room\n\twhen rendering with OpenGL? That said, the \u003ci\u003eprimary surface\u003c/i\u003e renderer\n\tseems to yield the most realistic numbers, as we still stay entirely within\n\tDirectDraw and perform no API wrapping. Using this renderer, I get speedups\n\tof roughly:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e~3.5× in the Music Room,\u003c/li\u003e\n\t\u003cli\u003e~1.9× during in-game dialog, and\u003c/li\u003e\n\t\u003cli\u003e~1.5× in the main menu.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNot bad for something I had to do anyway to port the game away from\n\tDirectDraw! Shuusou Gyoku is rather infamous among the vintage computer\n\tscene for being ridiculously unoptimized, so I should definitely be able to\n\tget some performance gains out of the in-game portion as well.\n\u003c/p\u003e\u003cp\u003e\n\tFor a final test of all the new blitting code, I also tried running\n\t\u003ci\u003eoutside\u003c/i\u003e DxWnd to verify everything against real and unpatched\n\tDirectDraw. Amusingly, this revealed how blitting from the new text surface\n\tseems to reach the color mapping limits of the DWM mitigation in 8-bit mode:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2023-08-01-SH01-8-bit-discoloration.png?3c0cbd35\" alt=\"\"\u003e\n\t\u003cfigcaption\u003e\n\t\tFor some reason, my system maps the intended \u003ccode\u003e#FFFFFF\u003c/code\u003e text\n\t\tcolor to \u003ccode\u003e#E4E3BB\u003c/code\u003e in the main menu?\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t8-bit mode does render correctly when I ran the same build in a Windows 98\n\tVirtualBox on the same system though, so it's not worth looking into a mode\n\tthat the system reports as unsupported to begin with. Let's leave this as\n\tsomewhat of a visual reminder for players to select 32-bit mode instead.\n\u003c/p\u003e\u003chr id=\"config-2023-08-01\"\u003e\u003cp\u003e\n\tAlright, enough about the annoying parts of GDI and DirectDraw for now.\n\tLet's stop looking back and start looking forward, to a time within this\n\tSeihou revolution when we're going to have lots of new options in the main\n\tmenu. Due to the nature of delivering individual pushes, we can expect lots\n\tof revisions to the config file format. Therefore, we'd like to have a\n\tbackward-compatible system that allows players to upgrade from any older\n\tbuild, including the original \u003ccode\u003e秋霜玉.exe\u003c/code\u003e, to a newer one. The\n\toriginal game predominantly used single-byte values for all its options, but\n\twe'd like our system to work with variables of any size, including strings\n\tto store things like \u003ca href=\"https://github.com/nmlgc/ssg/issues/14\"\u003ethe\n\tname of the selected MIDI device\u003c/a\u003e in a more robust way. Also, it's pure\n\tevil to reset the entire configuration just because someone tried to\n\thex-edit the config file and didn't keep the checksum in mind.\n\u003c/p\u003e\u003cp\u003e\n\tIt didn't take long for me to arrive at a common\n\t\u003ccode\u003eSize()\u003c/code\u003e/\u003ccode\u003eRead()\u003c/code\u003e/\u003ccode\u003eWrite()\u003c/code\u003e interface. By\n\tusing the same interface for both arrays and individual values, new config\n\tfile versions can naturally expand older ones by taking the array of option\n\treferences from the previous version and wrapping it into a new array,\n\ttogether with the new options.\u003cbr\u003e\n\tThe classic way of implementing this in C++ involves a typical\n\tobject-oriented class hierarchy: An \u003ccode\u003eOption\u003c/code\u003e base class would\n\tdefine the interface in the form of virtual abstract functions, and the\n\t\u003ccode\u003eValue\u003c/code\u003e, \u003ccode\u003eArray\u003c/code\u003e, and \u003ccode\u003eConfigVersion\u003c/code\u003e\n\tsubclasses would provide different implementations. This works, but\n\tintroduces quite a bit of boilerplate, not to mention the runtime bloat from\n\tall the virtual functions which Visual C++ can't inline. Why should we do\n\t\u003ci\u003eany\u003c/i\u003e runtime dispatch here? We know the set of configuration options\n\tat compile time, after all… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tLet's try looking into the modern C++ toolbox and see if we can do better.\n\tThe only real challenge here is that the array type has to support\n\tarbitrarily sized option value types, which sounds like a job for\n\t\u003ci\u003etemplate parameter packs\u003c/i\u003e. If we save these into a\n\t\u003ccode\u003estd::tuple\u003c/code\u003e, we can then \"iterate\" over all options with \u003ca\n\thref=\"https://en.cppreference.com/w/cpp/utility/apply\"\u003e\u003ccode\u003estd::apply\u003c/code\u003e\u003c/a\u003e\n\tand \u003ca href=\"https://en.cppreference.com/w/cpp/language/fold\"\u003efold\n\texpressions\u003c/a\u003e, in a nice functional style.\u003cbr\u003e\n\tI was amazed by just how clearly the \"crazy\" modern C++ approach with\n\ttemplate parameter packs, \u003ccode\u003estd::apply()\u003c/code\u003e over giant\n\t\u003ccode\u003estd::tuple\u003c/code\u003es, and fold expressions beats a classic polymorphic\n\thierarchy of abstract virtual functions. With the interface moved into an\n\teven optional \u003ccode\u003econcept\u003c/code\u003e, the class hierarchy can be completely\n\tflattened, which surprisingly also makes the code easier to both read and\n\twrite.\n\u003c/p\u003e\u003cp\u003e\n\tHere's how the new system works from the player's point of view:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe config files now use a kanji-less and explicitly forward-compatible\n\tnaming scheme, starting with \u003ccode\u003eSSG_V00.CFG\u003c/code\u003e in the P0251 build.\n\tThe format of this initial version simply includes all values from the\n\toriginal \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e without padding bytes or a checksum. Once\n\twe release a new build that adds new config options, we go up to\n\t\u003ccode\u003eSSG_V01.CFG\u003c/code\u003e, and so on.\u003c/li\u003e\n\t\u003cli\u003eWhen \u003ci\u003eloading\u003c/i\u003e, the game starts at its newest supported config file\n\tversion. If that file doesn't exist, the game retries with each older\n\tversion in succession until it reaches the last file in the chain, which is\n\talways the original \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e. This makes it possible to\n\tupgrade from any older Shuusou Gyoku build to a newer one while retaining\n\tall your settings – including, most importantly, which shot types you\n\tunlocked the Extra Stage with. The newly introduced settings will simply\n\tremain at their initial default in this case.\u003c/li\u003e\n\t\u003cli\u003eWhen \u003ci\u003esaving\u003c/i\u003e, the game always writes all versions it knows about,\n\tdown to and including the original \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e, in the\n\trespective version-specific format. This means that you can change options\n\tin a newer build and they'll show up changed in older builds as well if they\n\twere supported there.\u003cbr\u003e\n\tAnd yes, this also means that we can stop writing the unsupported 32-bit bit\n\tdepth setting to \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e, which would cause a validation\n\tfailure on the original build. This is now avoided by simply turning 32-bit\n\tinto 16-bit just for the configuration that gets saved to this file. And\n\tspeaking of validation failures…\u003c/li\u003e\n\t\u003cli\u003eThe \u003ccode\u003eSSG_V*.CFG\u003c/code\u003e files don't use checksums at all, which\n\tallows you to freely hex-edit them. Each configuration value is now\n\tvalidated individually, and reset to its default if you hex-edited it to\n\tsomething invalid. In the future, \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/12\"\u003ewe could even show an\n\tin-engine window at startup that lists these invalid options and the\n\tdefaults they were reset to, if we get backer support for this idea.\u003c/a\u003e\u003cul\u003e\n\t\t\u003cli\u003eThis per-value validation is also done if my builds loaded the\n\t\toriginal \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e. The checksum is still written for\n\t\tcompatibility with the original build, but my builds ignore it.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tWith that, we've got more than enough code for a new build:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0251\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0251\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tThis build also contains two more fixes that didn't fit into the big\n\tDirectDraw or configuration categories:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe P0226 build had a bug that allowed invalid stages to be selected for\n\treplay recording. If the \u003ccode\u003eReplaySave\u003c/code\u003e option was\n\t\u003ccode\u003e[O\u0026nbsp;F\u0026nbsp;F]\u003c/code\u003e, pressing the ⬅️ left arrow key on the\n\t\u003ccode\u003eStageSelect\u003c/code\u003e\n\toption would overflow its value to 255. The effects of this weren't all too\n\tserious: The game would simply stay on the Weapon Select screen for an\n\tinvalid stage number, or launch into the Extra Stage if you scrolled all the\n\tway to 131. Still, it's fixed in this build.\u003cfigure class=\"fullres\n\tpixelated\"\u003e\n\t\t\u003cimg src=\"/blog/static/2023-08-01-SH01-P0226-Replay-stage-select-255.png?4aa4b42b\" alt=\"Screenshot of the negative overflow bug that the P0226 build of Shuusou Gyoku accidentally introduced into the replay stage selection\"\u003e\n\t\t\u003cfigcaption\u003eWhoops! That one was fully my fault.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThe render time for the in-game music title is now roughly cut in half: \u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-08-01-SH01-Ingame-music-title-original.webp?7eb86565\" preload=\"none\" controls data-title=\"Original game\" data-active width=\"384\" height=\"160\" data-fps=\"60\" data-frame-count=\"194\" style=\"aspect-ratio: 384 / 160\" data-lossless=\"/blog/static/video/zmbv/2023-08-01-SH01-Ingame-music-title-original.avi?b26434e0\"\u003e\u003csource src=\"/blog/static/video/av1/2023-08-01-SH01-Ingame-music-title-original.webm?cb135bed\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-08-01-SH01-Ingame-music-title-original.webm?12420b4a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-08-01-SH01-Ingame-music-title-original.webm?9843f888\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's in-game music title animation in the original 秋霜玉.exe running in 8-bit mode under DxWnd without any tweaks, showing off how rendering the title takes a sluggish 8-frames, as well as how DxWnd's incorrect emulation of surface DCs  results in a wildly incorrect palette. \u003ca href=\"/blog/static/video/zmbv/2023-08-01-SH01-Ingame-music-title-original.avi?b26434e0\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Text rendering starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"71\" data-title=\"Gradient rendered\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-08-01-SH01-Ingame-music-title-P0251.webp?7eb86565\" preload=\"none\" controls data-title=\"P0251 build\" width=\"384\" height=\"160\" data-fps=\"60\" data-frame-count=\"191\" style=\"aspect-ratio: 384 / 160\" data-lossless=\"/blog/static/video/zmbv/2023-08-01-SH01-Ingame-music-title-P0251.avi?8b433b3a\"\u003e\u003csource src=\"/blog/static/video/av1/2023-08-01-SH01-Ingame-music-title-P0251.webm?2bf194aa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-08-01-SH01-Ingame-music-title-P0251.webm?8c8b83c6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-08-01-SH01-Ingame-music-title-P0251.webm?f0d9a89e\" type=\"video/webm\"\u003eVideo of Shuusou Gyoku's in-game music title animation in the P0251 build, showing off how it reduced the rendering time to 4 frames, and how it's unaffected by DxWnd's DC palette bug. \u003ca href=\"/blog/static/video/zmbv/2023-08-01-SH01-Ingame-music-title-P0251.avi?8b433b3a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Text rendering starts\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"67\" data-title=\"Gradient rendered\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tAchieved by simply trimming trailing whitespace and using slightly more\n\t\tefficient GDI functions to draw the gradient. Spending 4 frames on\n\t\trendering a gradient is still way too much though. I'll optimize that\n\t\tfurther once I actually get to port this effect away from GDI.\u003cbr\u003e\n\t\tThese videos also show off how DxWnd's DC palette bug affected the\n\t\toriginal game, and how it doesn't affect the P0251 build.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"sdl-2023-08-01\"\u003e\u003cp\u003e\n\tThese 6 pushes still left several of Shuusou Gyoku's DirectDraw portability\n\tissues unsolved, but I'd better look at them once I've set up a basic OpenGL\n\tskeleton to avoid any more premature abstraction. Since the ultimate goal is\n\ta Linux port, I might as well already start looking at the current best\n\tplatform layer libraries. SDL would be the standard choice here, and while\n\tSDL_ttf looks regrettably misdesigned, the core SDL library seems to cover\n\tall we could possibly want for Shuusou Gyoku, including a 2D renderer… wait,\n\t\u003ci\u003ewhat\u003c/i\u003e?!\n\u003c/p\u003e\u003cp\u003e\n\tYup. Admittedly, I've been living under a rock as far as SDL is concerned,\n\tand thus wasn't aware that SDL 2 introduced \u003ca\n\thref=\"https://wiki.libsdl.org/SDL2/SDL_Renderer\"\u003eits own abstraction for 2D\n\trendering\u003c/a\u003e that just happens to almost exactly cover everything we need\n\tfor Shuusou Gyoku. This API even covers all of the game's Direct3D code,\n\twhich only draws alpha-blended, untextured, and pre-transformed\n\tvertex-colored triangles and lines. It's the exact abstraction over OpenGL I\n\tthought I had to write myself, and such a perfect match for this game that\n\tit would be foolish to go for a custom OpenGL backend – especially since SDL\n\twill automatically target the ideal graphics API for any given operating\n\tsystem.\n\u003c/p\u003e\u003cp\u003e\n\tSadly, the one thing SDL_Renderer is missing is something equivalent to\n\tpixel shaders, which we would need to replicate the \u003cspan lang=\"ja\"\u003e西方Ｐｒｏｊｅｃｔ\u003c/span\u003e lens ball effect shown at startup. Looks like we have\n\tto drop into \u003ca href=\"https://wiki.libsdl.org/SDL2/SDL_GetWindowSurface\"\u003ea\n\tcompletely separate, unaccelerated rendering mode\u003c/a\u003e and continue to\n\tsoftware-render this one effect before switching to hardware-accelerated\n\trendering for the rest of the game. But at least we \u003ci\u003ecan\u003c/i\u003e do that in a\n\tcross-platform way, and \u003ci\u003edon't\u003c/i\u003e have to bother with shading languages –\n\tor, perhaps even worse, \u003ca href=\"https://xkcd.com/927/\"\u003eSDL's own shading\n\tlanguage\u003c/a\u003e.\u003cbr\u003e\n\tIf we were extremely pedantic, we'd also have to do the same for the\n\t\u003ca href=\"/blog/2022-12-31\"\u003e📝 unused spiral effect that was originally intended for the staff roll\u003c/a\u003e.\n\tSoftware rendering would be even more annoying there, since we don't\n\t\u003ci\u003ejust\u003c/i\u003e have to software-render these staff sprites, but also the ending\n\tpicture and text, complete with their respective fade effects. And while I\n\ttypically do go the extra mile to preserve whatever code was present in\n\tthese games, keeping \u003ci\u003ethis\u003c/i\u003e effect would just needlessly drive up the\n\tcost of the SDL backend. Let's just move this one to the \u003ca\n\thref=\"https://github.com/nmlgc/ssg/tree/master/unused\"\u003emuseum of unused\n\tcode\u003c/a\u003e and no longer actively compile it. RIP spiral 🥲 At least you're\n\tstill preserved in lossless video form.\n\u003c/p\u003e\u003cp\u003e\n\tNow that SDL has become an integral part of Shuusou Gyoku's portability plan\n\trather than just being one potential platform layer among many, the optimal\n\torder of tasks has slightly changed. If we stayed within the raw Win32 API\n\tany longer than absolutely necessary, we'd only risk writing  more\n\tWin32-native code for things like \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/9\"\u003eaudio streaming\u003c/a\u003e that we'd\n\tthen have to throw away and rewrite in SDL later. Next up, therefore:\n\tStaying with Shuusou Gyoku, but continuing in a much more focused manner by\n\tfixing the input system and starting the SDL migration with input and sound.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-09-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-07-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-08-01T23:57:43Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-07-28",
      "url": "https://rec98.nmlgc.net/blog/2023-07-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-08-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-07-28\"\u003e\u003ctime datetime=\"2023-07-28T14:00:00Z\"\u003e2023-07-28\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tYet another small interruption before we get to Shuusou Gyoku, but only\n\tbecause I've got a big announcement to make! \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e has just\n\tcommissioned the basic feature set that would allow PC-98 Touhou to be\n\ttranslated into non-ASCII languages. 💰 And we're in fact doing it on PC-98,\n\tand don't wait for the games to be ported to other systems first.\n\u003c/p\u003e\u003ch5\u003eHow is this going to work?\u003c/h5\u003e\u003cp\u003e\n\tThis project will start sometime after I've completed the current big\n\tproject of \u003ca href=\"https://github.com/nmlgc/ssg/issues/42\"\u003eporting Shuusou\n\tGyoku to Linux\u003c/a\u003e, so probably during the summer of 2024. Similar to\n\tthe previous MediaWiki update, this will bypass the ReC98 push and cap\n\tmodel: \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e is going to guarantee a minimum budget out of\n\ttheir Open Collective funds, which can be increased with further donations\n\tfrom the community, and I'm going to send an invoice once I'm done. In\n\taddition, I'm also going to keep in contact with all interested translators\n\tand backers via a Discord room throughout the process for additional\n\ttechnical quality control.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2024-04-11):\u003c/strong\u003e Over the last few months, I've focused all unconstrained RE funding on increasing the amount of moddable text-related code. As a result, the translation project could now cover the majority of text in PC-98 Touhou, including:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAll of TH01\u003c/li\u003e\n\t\u003cli\u003eAll of TH02's \u003ccode\u003eOP.EXE\u003c/code\u003e, i.e., the main menu\u003c/li\u003e\n\t\u003cli\u003eAll Music Rooms (\u003ca href=\"/blog/2024-02-03\"\u003e📝 completed in P0265\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eTH02's, TH04's, and TH05's in-game dialog\n\t(\u003ca href=\"/blog/2023-11-01\"\u003e📝 completed in P0261\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eTH03's win messages\n\t(\u003ca href=\"/blog/2023-11-01\"\u003e📝 completed in P0261\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eTH04's and TH05's first-launch sound setup menu (\u003ca href=\"/blog/2023-11-30\"\u003e📝 completed in P0263\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eTH04's and TH05's main menu (\u003ca href=\"/blog/2023-11-30\"\u003e📝 completed in P0262\u003c/a\u003e)\u003c/li\u003e\n\t\u003cli\u003eTH02's, TH04's and TH05's endings\u003c/li\u003e\n\t\u003cli\u003eTH02's verdict screen (\u003ca href=\"/blog/2024-04-11\"\u003e📝 completed in P0279\u003c/a\u003e)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWith still a bit of time left until the Shuusou Gyoku Linux port is done,\n\tI'll put any general and unconstrained reverse-engineering,\n\tposition independence, or \u003cq\u003eanything\u003c/q\u003e contributions that come in during\n\tthe next few months towards covering everything that's still missing there:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eTH04's and TH05's \u003ccode\u003eMAINE.EXE\u003c/code\u003e contains some not\n\tyet RE'd text in their verdict screens. 1.5 pushes there, since it's\n\tunfortunately contained in the same function that also performs the highly\n\tcomplex skill value calculation.\u003c/li\u003e\n\t\u003cli\u003eTH05's Extra Stage ending is followed by an \u003ci\u003eAll Cast\u003c/i\u003e screen listing the characters of all 5 games, to the tune of \u003ca lang=\"ja\" href=\"https://www.youtube.com/watch?v=-BZUMv5gAkk\"\u003ePeaceful Romancer\u003c/a\u003e. Shouldn't take longer than 0.5 pushes.\u003c/li\u003e\n\t\u003cli\u003eTH03's \u003ccode\u003eMAINL.EXE\u003c/code\u003e needs 100% PI to enable convenient\n\ttranslations of the win messages, the character titles and names at the\n\tbeginning of a stage, the Stage 8/9 cutscenes, and the endings. Let's go\n\twith 2 pushes there just to be safe, and finalize the missing code to not \u003ca href=\"/blog/2021-12-15\"\u003e📝 incur more technical debt\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eTechnically, we'd need TH02's \u003ccode\u003eMAIN.EXE\u003c/code\u003e to be 100%\n\tposition-independent for any translation-related code modifications, but\n\treaching that goal before I get to work on translation support is probably\n\tunrealistic. However, this new translation code needs to work across \u003cspan\n\tclass=\"hovertext\" title=\"1 for TH01's Anniversary Edition, and 3 executables for the other 4 games\"\u003e13\n\texecutables\u003c/span\u003e to begin with, so I'm going to put most of it into a\n\tseparate TSR program anyway. Including this TSR in a non-PI'd executable\n\tshouldn't be that painful, then.\u003c/li\u003e\n\t\u003cli\u003eThe same is true for TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e, but the \u003cspan\n\tlang=\"ja\"\u003eＷＩＮＮＥＲ ＢＯＮＵＳ\u003c/span\u003e popup is the only translatable piece of text there. Should be even less of a problem.\u003c/li\u003e\n\t\u003cli\u003eTH04's and TH05's High Score menus contain a single string about scores not being recorded in Slow Mode (\u003ccode\u003eスローモードでのプレイでは、スコアは記録されません\u003c/code\u003e). Regularly, this means that we'd have to decompile the whole menu, together with TH05's intricate \"glyph ball\" animation, which would be way too excessive just for this one string. If the Shuusou Gyoku Linux port gets done sooner than this gets decompiled, I'll figure something out.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn total, that's the next 4 general pushes that will go towards ensuring\n\ttranslatability of most of PC-98 Touhou. If you'd like your\n\tcontribution (or existing subscription) to go to \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e code instead, be sure to tell me!\n\u003c/p\u003e\u003ch5\u003eWhat's the minimum guaranteed set of features?\u003c/h5\u003e\u003cp\u003e\n\tThe main feature will be a custom renderer for a subsetted, monospaced\n\tUnicode bitmap font, and its integration into any translatable part of the\n\tgame. For the script files, this means UTF-8 support with Shift-JIS\n\tfallback. For the glyphs, I'll use \u003ca\n\thref=\"https://unifoundry.com/unifont/\"\u003eGNU Unifont\u003c/a\u003e by default, but we\n\tcould also use any other freely licensed bitmap font with 8×16 or 16×16\n\tglyphs for alphabets of certain languages. Everything about this will be the\n\treal deal: The system will potentially support all of Unicode without font\n\tROM hacks so that the translations will work on real hardware, and there\n\twill be no shortcuts for just a few Latin characters. And if someone wants\n\tto translate this game into a language with \u003ca\n\thref=\"https://twitter.com/cmuratori/status/1416978255584792579\"\u003emore complex\n\tshaping rules\u003c/a\u003e, I'll make sure that they look pretty as well if there's\n\tsome budget left.\u003cbr\u003e\n\tThis will allow translation teams to build static translation patches into\n\tany language by editing the original script files, and using\n\t\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e's \u003ca href=\"http://lunarcast.net/mystictk.php\"\u003eexisting\n\ttools\u003c/a\u003e for any images. Modifications of hardcoded strings would still\n\trequire recompiling the binary, and each group would have to distribute and\n\tadvertise the result on their own.\n\u003c/p\u003e\u003ch5\u003e\n\t\u003cspan style=\"font-style: normal;\"\u003e🌐\u003c/span\u003e\n\tWhich languages are we getting?\n\u003c/h5\u003e\u003cp\u003e\n\tAs of 2023-10-10, the following translators and teams have expressed\n\tinterest:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cb\u003eArabic\u003c/b\u003e: Team Fantasy Boundary\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eChinese, Simplified\u003c/b\u003e: ROCO2018, XiTieShiZ, Yanstime (Enko)\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eChinese, Traditional\u003c/b\u003e: Haniyasuko Okina\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eCroatian\u003c/b\u003e: \u003ca\n\thref=\"https://www.thpatch.net/wiki/Portal:Hr\"\u003eTRDario\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eEnglish, literal\u003c/b\u003e: \u003ca\n\thref=\"https://www.thpatch.net/wiki/Portal:En-literal\"\u003eYova\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eFrench\u003c/b\u003e: Lance, Slime900\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eGaelic\u003c/b\u003e: nitori\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eGerman\u003c/b\u003e: \u003ca href=\"https://rd.mangadex.com/\"\u003eSplashman / Reality\n\tDreamers\u003c/a\u003e, PK Eager Maribel\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eGreek\u003c/b\u003e: Tasos500\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eHungarian\u003c/b\u003e: \u003ca\n\thref=\"https://www.thpatch.net/wiki/Portal:Hu\"\u003eSpectatorsatori\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eItalian\u003c/b\u003e: \u003ca\n\thref=\"https://www.thpatch.net/wiki/Portal:It\"\u003eShin\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003ePolish\u003c/b\u003e: Matt\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eRomanian\u003c/b\u003e: Adi125\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eRussian\u003c/b\u003e: \u003ca href=\"https://raincat.4otaku.org/\"\u003eCyrusVorazan\u003c/a\u003e,\n\tBadass1987\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eScots\u003c/b\u003e: nitori\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eSerbian, literal\u003c/b\u003e: \u003ca\n\thref=\"https://www.thpatch.net/wiki/Portal:Sr-literal\"\u003eYova\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eSpanish, Argentinean\u003c/b\u003e: Mr. Tremolo Measure\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eSpanish, Latin American\u003c/b\u003e: Xziled, DarkeyeSide, Mr. Tremolo\n\tMeasure\u003c/li\u003e\n\t\u003cli\u003e\u003cb\u003eVietnamese\u003c/b\u003e: Shinka\u003c/li\u003e\n\u003c/ul\u003e\u003ch5\u003eWait, Arabic?! On my PC-98?! What's the plan there?\u003c/h5\u003e\u003cp\u003e\n\tThe two challenges with Arabic scripts are \u003ca\n\thref=\"https://en.wikipedia.org/w/index.php?title=Arabic_script_in_Unicode\u0026oldid=1179025150#Contextual_forms\"\u003etransforming\n\ta text to use the codepoints for contextual glyph forms\u003c/a\u003e\n\t(\u003cq\u003eshaping\u003c/q\u003e), and right-to-left rendering. Shaping requires \u003ca\n\thref=\"https://github.com/eloraiby/arabtype/blob/master/arabtype.c\"\u003enot too\n\tmuch code\u003c/a\u003e, which is easily added to the font subsetting build step.\n\tRight-to-left rendering, on the other hand, must be a feature of the new\n\tPC-98-native text renderer, because there are several places in PC-98 Touhou\n\twhere text is gradually typed character-by-character. So it will require a\n\tbit of dedicated budget, but not all too much from what I can tell. \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Bidirectional_text\"\u003eBidirectional\n\ttext\u003c/a\u003e \u003ci\u003ewould\u003c/i\u003e add a great deal of complexity here, but we most\n\tlikely won't need to implement it – I'll simply pick a direction based\n\ton \u003cspan class=\"hovertext\" title=\"Which can be a U+200F RIGHT-TO-LEFT MARK if the line starts with a non-Arabic-script character.\"\u003ethe\n\tfirst codepoint on a line\u003c/span\u003e, and ask translators to manually reverse\n\tany Latin-script runs of text in the middle of an Arabic-script line.\n\u003c/p\u003e\u003ch5\u003eHow much better could it all be?\u003c/h5\u003e\u003cul\u003e\n\t\u003cli\u003eThe most important feature: We could finally move away from the concept\n\tof \u003ci\u003etranslation patches\u003c/i\u003e, integrate all translations as part of the\n\tReC98 repo, and ship them directly as part of new ReC98 builds. Languages\n\tcould then be switched at runtime, through a new setting in the Option\n\tmenu.\u003cul\u003e\n\t\t\u003cli\u003eAnd why stop there? How about binding a keyboard key to a new\n\t\tlanguage selection window that can be opened at any point during the\n\t\tgame, and even switches out any text that is currently shown on\n\t\tscreen?\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eI could \u003ca\n\thref=\"https://github.com/thpatch/thcrap/issues/38\"\u003efinally\u003c/a\u003e translate a\n\tcanon Touhou game via a \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Gettext\"\u003egettext\u003c/a\u003e-like dictionary\n\tsystem. This would allow modded source text to override translations, and\n\teven make it possible to translate mods as well.\u003c/li\u003e\n\t\u003cli\u003eIdeally, all translators I get to work with are highly motivated and\n\tfinish translating each game they start, so that we don't even have to think\n\tabout \u003ca\n\thref=\"https://www.thpatch.net/wiki/Touhou_Patch_Center:About#Patch_stacking\"\u003etranslation\n\tstacking\u003c/a\u003e, but maybe we still should.\u003c/li\u003e\n\t\u003cli\u003eWe could use proportional fonts instead of aligning every glyph to the\n\t8×16 text RAM grid. Unlike the Windows Touhou games where proportional fonts\n\tare crucial because adding more text space would desync replays, they are\n\tnot \u003ci\u003ethat\u003c/i\u003e important in PC-98 Touhou. With no replays to be desynced,\n\twe can arbitrarily add new boxes without worrying about the font.\u003cul\u003e\n\t\t\u003cli\u003eHowever, supporting proportional fonts would make it possible to\n\t\tlift some of the text sprites into the custom font system, allowing\n\t\ttheir glyphs to be shared more easily across languages:\u003cfigure\u003e\u003cfigure\n\t\tclass=\"side_by_side pixelated\"\u003e\n\t\t\t\u003cimg src=\"/blog/static/2023-07-28-TH03-gaiji.png?811404fc\" alt=\"TH03's gaiji text.\"\u003e\n\t\t\t\u003cimg src=\"/blog/static/2023-07-28-TH05-SFT2.png?ed7c238d\" alt=\"TH04's and TH05's SFT2.CDG, containing the image text used in the main menus.\"\u003e\n\t\t\u003c/figure\u003e\u003cfigcaption\u003e\n\t\t\tSome of the image text from TH03's, TH04's, and TH05's main menus.\n\t\t\tTurning these sprites into text so that translators won't have to\n\t\t\tmanually shift pixels around may or may not be worth it.\n\t\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eOn the topic of new text boxes: Automatic line and box breaks at word\n\tboundaries would completely remove the need for in-game proofreading.\u003c/li\u003e\n\t\u003cli\u003eIn-game TL notes… nah, probably not. Where would we even put them in the\n\toriginal screen layout?\u003c/li\u003e\n\t\u003cli\u003eDue to the continued interest in TH01's Anniversary Edition, any code\n\tmodifications would be exclusive to a respective game's bugfixed\n\t\u003ccode\u003eanniversary\u003c/code\u003e branch – i.e., any translated builds will be\n\tbundled with a growing number of fixes for issues in the original games that\n\tfall under \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-bug\"\u003emy\n\tcurrent definition of \u003ci\u003ebugs\u003c/i\u003e\u003c/a\u003e. This avoids a combinatorial explosion\n\tof the number of branches, merges, and releases I'd have to do. For a small\n\tamount of extra money though, I could merge them back to the ZUN\n\tbug-preserving \u003ccode\u003edebloated\u003c/code\u003e branch. And for a \u003ci\u003elot\u003c/i\u003e of extra\n\tmoney, I could reimplement everything on \u003ccode\u003emaster\u003c/code\u003e while\n\tpreserving the original memory layout of ZUN's original binaries. This would\n\tallow the translation-supporting binaries to be easily diffed against the\n\toriginal ones, and retain compatibility with existing hacks or cheat tables.\n\tThe latter was already something that\n\t\u003ca href=\"/blog/2023-05-29\"\u003e📝 the Shuusou Gyoku community previously expected my recompiled builds to have\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eJust like with \u003ca href=\"/blog/tag/mod\"\u003eall custom PC-98 Touhou game\n\tbuilds\u003c/a\u003e I've shipped so far, you would install the translations by \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/releases/tag/P0234\"\u003emanually dropping\n\tfiles into the game directory, or editing .HDI images using third-party\n\ttools\u003c/a\u003e. I can make this step pretty much arbitrarily more user-friendly\n\tby writing nice installers that cover all imaginable setups and use\n\tcases.\u003c/li\u003e\n\t\u003cli\u003eI could top off the project with some smaller, more intricate\n\tlocalizations that translators might request for certain languages. Most\n\tnotably, this category would include any localization of TH01's\n\t\u003cspan lang=\"ja\"\u003e東方★靈異伝\u003c/span\u003e,\n\t\u003cspan style=\"color: red\"\u003eSTAGE #\u003c/span\u003e, and\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e popups that goes beyond just\n\tfixing the Engrish.\u003c/li\u003e\n\t\u003cli\u003eThe previous static English patches from 2014 introduced quite a few\n\tfanfiction changes that have been interpreted as canon in the years since. I\n\tcould write a blog post to highlight these, and also compare the translation\n\tas a whole with the more literal English translation we're likely to get\n\tthis time around.\u003c/li\u003e\n\t\u003cli\u003eFinally, if you all \u003ci\u003ereally\u003c/i\u003e want to, I could move all translatable\n\tcontent to the \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e interface, which would truly turn that\n\tsite into the one central translation source for all canon Touhou games.\n\tAutomatic updates won't be feasible before porting away the games from PC-98\n\thardware, so the thpatch server would have to communicate with the ReC98\n\trepo via a GitHub webhook. This will be rather expensive though, as I'd also\n\thave to set up some kind of build/release CI for ReC98 first.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tYou might remember most of this from\n\t\u003ca href=\"/blog/2022-11-30\"\u003e📝 my initial pitch back in November\u003c/a\u003e, but I\n\tdid have quite a bit of additional ideas since then.\n\u003c/p\u003e\u003cp\u003e\n\tThese features are mostly independent of each other, and it will be up to\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e to pick a priority order. That's also where all of you\n\tcould come in and influence this order with your donations. So it's closer\n\tto a traditional crowdfunding campaign with stretch goals, where the sky is\n\tthe limit, than it is to the usual ReC98 model. And while there can be no\n\tfixed prices for any of the goals, you can be sure that anything you invest\n\twill improve the quality of the final product.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca\n\t\tclass=\"release\" href=\"https://opencollective.com/thpatch\"\n\t\u003e\u003cimg src=\"/static/emoji-opencollective.svg?2757583d\" alt=\":opencollective:\" width=\"24\" height=\"24\" \u003e Touhou Patch Center on Open Collective\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tFrom now on, this will be the only way of funding any translation-related\n\tgoals; I've removed the respective options from the ReC98 order form.\n\tLooking forward to how many of these additional ideas I get to implement –\n\tbut, as always, please invest responsibly.\n\u003c/p\u003e\u003cp\u003e\n\tShuusou Gyoku finally coming this weekend.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-08-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-07-28T16:00:00+02:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-06-30",
      "url": "https://rec98.nmlgc.net/blog/2023-06-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-07-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-06-30\"\u003e\u003ctime datetime=\"2023-06-30T23:52:16Z\"\u003e2023-06-30 23:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0245\"\u003eP0245\u003c/a\u003e\n\t\t\tTH04/TH05 finalization (Sprite clipping + gather circles + boss explosions) + TH01 Anniversary Edition (Lines, part 1/?)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/97f0c3b...5876755\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, Ember2528, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debloating\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Cleaning up ZUN\u0026#39;s original code into something you can actually read and maintain.\"\u003edebloating\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\tfigure#scroll-2023-06-30 button:first-child {\n\t\tpadding-left: revert;\n\t}\n\n\t#sheet-2023-06-30 img {\n\t\twidth: 512px;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tAnd then, the supposed boilerplate code revealed yet another confusing issue\n\tthat quickly forced me back to serial work, leading to no parallel progress\n\tmade with Shuusou Gyoku after all. 🥲 The list of functions I put together\n\tfor the first ½ of this push seemed so boring at first, and I was so sure\n\tthat there was almost nothing I could possibly talk about:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eTH02's gaiji animations at the start and end of each stage, resembling\n\topening and closing window blind slats. ZUN should have maybe not defined\n\tthe regular whitespace gaiji as what's technically the last frame of the\n\tclosing animation, but that's a minor nitpick. Nothing special there\n\totherwise.\u003c/li\u003e\n\t\u003cli\u003eThe remaining spawn functions for TH04's and TH05's gather circles. The\n\tonly dumb antic there is the way ZUN initializes the template for bullets\n\tfired at the end of the animation, featuring ASM instructions that are\n\tequivalent to what Turbo C++ 4.0J generates for the \u003ccode\u003e__memcpy__\u003c/code\u003e\n\tintrinsic, but show up in a different order. Which means that they must have\n\tbeen handwritten. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e I already figured that out in 2022\n\tthough, so this was just more of the same.\u003c/li\u003e\n\t\u003cli\u003eEX-Alice's override for the game's main 16×16 sprite sheet, loaded\n\tduring her dialog script. More of a naming and consistency challenge, if\n\tanything.\n\t\u003cfigure id=\"sheet-2023-06-30\" class=\"pixelated checkerboard\"\u003e\n\t\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\t\tThe regular version of TH05's big 16×16 sprite sheet.\n\t\t\u003c/div\u003e\u003cdiv\u003e\n\t\t\tEX-Alice's variant of TH05's big 16×16 sprite sheet.\n\t\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-06-30-TH05-MIKO16.BFT.png?eb8a7354\"\n\t\t\tdata-title=\"\u003ccode\u003eMIKO16.BFT\u003c/code\u003e\"\n\t\t\talt=\"\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-06-30-TH05-ST06_16.BFT.png?672ba3f0\"\n\t\t\tdata-title=\"\u003ccode\u003eST06_16.BFT\u003c/code\u003e\"\n\t\t\talt=\"\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThe rendering function for TH04's Stage 4 midboss, which seems to\n\tfeature the same premature clipping quirk we've seen for\n\t\u003ca href=\"/blog/2022-11-30\"\u003e📝 TH05's Stage 5 midboss, 7 months ago\u003c/a\u003e?\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cimg class=\"inline_sprite\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAFVBMVEUAAAD/7+//78/vuqq6urr/RUWKAAALp9jpAAAAAXRSTlMAQObYZgAAATlJREFUeNp9VEFuhDAMtHYr7mz7gF2HFyTLvYX+AJkzF/z/J1R2GAks6JxCxpOxYwciYDT8UgRRZmZOcZfGsTiihjJXImgsXhxBY/Eg9poxFRF1iJT8xj7lIwEJjaUX1bpWlanAJUciwaGfFWoiXafNJUdiqi4fqZ+XfVHNOuVvIrqfEU8iGorYQWrGfhSplM5zioSK5QULIhWLUNFqcj8nnvQDQqX4oerunVcRiXXKn0jK9pmxlsJIyj5eD6wtLZQhhV8t4wOKKmjbBz4OigOxV3CLo/aKKrnIap9uqpU3CwrEvTMN58QX3ayx67zgEtFCv/ZTwt2beo1omDfK842Et/Z2NSVX43M1cOcj2v0/1PEZTKWDejg+HL56ahBcPc5NAqIkCDYb/ACYjjCNIcRXjSHuuialXfwfWKCn4OUTyOMAAAAASUVORK5CYII=\"\u003e\n\tThe rendering function for the big 48×48 explosion sprite, which \u003ci\u003ealso\u003c/i\u003e\n\tfeatures the same clipping quirk?\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThat's three instances of ZUN removing sprites way earlier than you'd want\n\tto, intentionally deciding against those sprites flying smoothly in and out\n\tof the playfield. Clearly, there has to be a system and a reason behind it.\n\u003c/p\u003e\u003cp\u003e\n\tTurns out that it can be almost completely blamed on master.lib. None of the\n\t\u003ccode\u003esuper_*()\u003c/code\u003e sprite blitting functions can clip the rendered\n\tsprite to the edges of VRAM, and much less to the custom playfield rectangle\n\twe would actually want here. This is exactly the wrong choice to make for a\n\tgame engine: Not only is the game developer now stuck with either rendering\n\tthe sprite in full or not at all, but they're also left with the burden of\n\tmanually calculating \u003ci\u003ewhen\u003c/i\u003e not to display a sprite.\u003cbr\u003e\n\tHowever, strictly limiting the top-left screen-space coordinate to\n\t(﻿0,\u0026nbsp;0﻿) and the bottom-right one to (﻿640,\u0026nbsp;400﻿) would actually\n\tstop rendering some of the sprites much earlier than the clipping conditions\n\twe encounter in these games. So what's going on there?\n\u003c/p\u003e\u003cp\u003e\n\tThe answer is a combination of playfield borders, hardware scrolling, and\n\tmaster.lib needing to provide at least \u003ci\u003esome\u003c/i\u003e help to support the\n\tlatter. Hardware scrolling on PC-98 works by dividing VRAM into two vertical\n\tpartitions along the Y-axis and telling the GDC to display one of them at\n\tthe top of the screen and the other one below. The contents of VRAM remain\n\tunmodified throughout, which raises the interesting question of how to deal\n\twith sprites that reach the vertical edges of VRAM. If the top VRAM row that\n\tstarts at offset \u003ccode\u003e0x0000\u003c/code\u003e ends up being displayed \u003ci\u003ebelow\u003c/i\u003e\n\tthe bottom row of VRAM that starts at offset \u003ccode\u003e0x7CB0\u003c/code\u003e for 399 of\n\tthe 400 possible scrolling positions, wouldn't we then need to vertically\n\twrap most of the rendered sprites?\u003cbr\u003e\n\tFor this reason, master.lib provides the \u003ccode\u003esuper_roll_*()\u003c/code\u003e\n\tfunctions, which unconditionally perform exactly this vertical wrapping. But\n\tthis creates a new problem: If these functions still can't clip, and don't\n\teven know which VRAM rows currently correspond to the top and bottom row of\n\tthe screen (since master.lib's \u003ccode\u003egraph_scrollup()\u003c/code\u003e function\n\tdoesn't retain this information), won't we also see sprites wrapping around\n\tthe \u003ci\u003eactual\u003c/i\u003e edges of the screen? That's something we certainly\n\twouldn't want in a vertically scrolling game…\u003cbr\u003e\n\tThe answer is yes, and master.lib offers no solution for this issue. But\n\tthis is where the playfield borders come in, and helpfully cover 16 pixels\n\tat the top and 16 pixels at the bottom of the screen. As a result, they can\n\thide up to 32 rows of potentially wrapped sprite pixels below them:\n\u003c/p\u003e\u003cfigure id=\"scroll-2023-06-30\" class=\"pixelated fullres\"\u003e\u003cfigcaption\u003e\n\t\u003cbutton\n\t\tid=\"scroll-0-2023-06-30\" onclick=\"\n\t\tdocument.getElementById('vram-0-2023-06-30').classList.add('active');\n\t\tdocument.getElementById('tram-0-2023-06-30').classList.add('active');\n\t\tdocument.getElementById('vram-200-2023-06-30').classList.remove('active');\n\t\tdocument.getElementById('tram-200-2023-06-30').classList.remove('active');\n\n\t\tdocument.getElementById('scroll-0-2023-06-30').hidden = true;\n\t\tdocument.getElementById('scroll-200-2023-06-30').hidden = false;\n\t\" hidden\u003e(Scroll by 200 pixels)\u003c/button\u003e\u003cbutton\n\t\tid=\"scroll-200-2023-06-30\" onclick=\"\n\t\tdocument.getElementById('vram-200-2023-06-30').classList.add('active');\n\t\tdocument.getElementById('tram-200-2023-06-30').classList.add('active');\n\t\tdocument.getElementById('vram-0-2023-06-30').classList.remove('active');\n\t\tdocument.getElementById('tram-0-2023-06-30').classList.remove('active');\n\t\tdocument.getElementById('scroll-200-2023-06-30').hidden = true;\n\t\tdocument.getElementById('scroll-0-2023-06-30').hidden = false;\n\t\"\u003e(Scroll by 200 pixels)\u003c/button\u003e • \u003cbutton\n\t\tid=\"tram-hide-2023-06-30\" onclick=\"\n\t\tdocument.getElementById('tram-0-2023-06-30').hidden = true;\n\t\tdocument.getElementById('tram-200-2023-06-30').hidden = true;\n\t\tdocument.getElementById('tram-hide-2023-06-30').hidden = true;\n\t\tdocument.getElementById('tram-show-2023-06-30').hidden = false;\n\t\" hidden\u003e(Hide text layer)\u003c/button\u003e\u003cbutton\n\t\tid=\"tram-show-2023-06-30\" onclick=\"\n\t\tdocument.getElementById('tram-0-2023-06-30').hidden = false;\n\t\tdocument.getElementById('tram-200-2023-06-30').hidden = false;\n\t\tdocument.getElementById('tram-show-2023-06-30').hidden = true;\n\t\tdocument.getElementById('tram-hide-2023-06-30').hidden = false;\n\t\"\u003e(Show text layer)\u003c/button\u003e\u003cbr\u003e\n\tThe earliest possible frame that TH05 can start rendering the Stage 5\n\tmidboss on. Hiding the text layer reveals how master.lib did in fact\n\t\"blindly\" render the top part of her sprite to the bottom of the\n\tplayfield. That's where her sprite \u003ci\u003estarts\u003c/i\u003e before it is correctly\n\twrapped around to the top of VRAM.\u003cbr\u003e\n\tIf we scrolled VRAM by another 200 pixels (and faked an equally shifted\n\tTRAM for demonstration purposes), we get an equally valid game scene\n\tthat points out why a vertically scrolling PC-98 game must wrap all sprites\n\tat the vertical edges of VRAM to begin with.\n\t\u003cbr\u003e\n\tAlso, note how the HP bar has filled up quite a bit before the midboss can\n\tactually appear on screen.\n\u003c/figcaption\u003e\u003cdiv class=\"multilayer\"\u003e\n\t\u003cimg\n\t\tid=\"vram-0-2023-06-30\"\n\t\tclass=\"active\"\n\t\tsrc=\"/blog/static/2023-06-30-TH05-Stage-5-midboss-clipping-VRAM-0.png?011f6579\"\n\t\talt=\"VRAM contents of the first possible frame that TH05's Stage 5 midboss can appear on, at their original scrolling position. Also featuring the 64×64 bounding box of the midboss sprite.\"\n\t\u003e\u003cimg\n\t\tid=\"vram-200-2023-06-30\"\n\t\tsrc=\"/blog/static/2023-06-30-TH05-Stage-5-midboss-clipping-VRAM-200.png?f944d0cc\"\n\t\talt=\"VRAM contents of the first possible frame that TH05's Stage 5 midboss can appear on, scrolled down by a further 200 pixels. Also featuring the 64×64 bounding box of the midboss sprite.\"\n\t\u003e\u003cimg\n\t\tid=\"tram-0-2023-06-30\"\n\t\tclass=\"active\"\n\t\tsrc=\"/blog/static/2023-06-30-TH05-Stage-5-midboss-clipping-TRAM-0.png?9b0283c7\"\n\t\talt=\"TH05's in-game text layer, at its original position.\"\n\t\thidden\n\t\u003e\u003cimg\n\t\tid=\"tram-200-2023-06-30\"\n\t\tsrc=\"/blog/static/2023-06-30-TH05-Stage-5-midboss-clipping-TRAM-200.png?e304540a\"\n\t\talt=\"TH05's in-game text layer, scrolled by 200 pixels to match an equally scrolled VRAM.\"\n\t\thidden\n\t\u003e\n\u003c/div\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that's how the lowest possible top Y coordinate for sprites blitted\n\tusing the master.lib \u003ccode\u003esuper_roll_*()\u003c/code\u003e functions during the\n\tscrolling portions of TH02, TH04, and TH05 is not 0, but -16. Any lower, and\n\tyou would \u003ci\u003eactually\u003c/i\u003e see some of the sprite's upper pixels at the\n\tbottom of the playfield, as there are no more opaque black text cells to\n\tcover them. Theoretically, you \u003ci\u003ecould\u003c/i\u003e lower this number for\n\t\u003ci\u003esome\u003c/i\u003e animation frames that start with multiple rows of transparent\n\tpixels, but I thankfully haven't found any instance of ZUN using such a\n\thack. So far, at least… \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tVisualized like that, it all looks quite simple and logical, but for days, I\n\tdid \u003ci\u003enot\u003c/i\u003e realize that these sprites were rendered to a scrolling VRAM.\n\tThis led to a much more complicated initial explanation involving the\n\tinvisible extra space of VRAM between offsets \u003ccode\u003e0x7D00\u003c/code\u003e and\n\t\u003ccode\u003e0x7FFF\u003c/code\u003e that effectively grant a hidden additional 9.6 lines\n\tbelow the playfield. Or even above, since PC-98 hardware ignores the highest\n\tbit of any offset into a VRAM bitplane segment\n\t(\u003ccode\u003e\u0026amp;\u0026nbsp;0x7FFF\u003c/code\u003e), which prevents blitting operations from\n\taccidentally reaching into a different bitplane. Together with the\n\taforementioned rows of transparent pixels at the top of these midboss\n\tsprites, the math would have almost worked out exactly.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tThe need for manual clipping also applies to the X-axis. Due to the lack of\n\tscrolling in this dimension, the boundaries there are much more\n\tstraightforward though. The minimum left coordinate of a sprite can't fall\n\tbelow 0 because any smaller coordinate would wrap around into the\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 tile source area\u003c/a\u003e and overwrite some of the\n\tpixels there, which we obviously don't want to re-blit every frame.\n\tSimilarly, the right coordinate must not extend into the HUD, which starts\n\tat 448 pixels.\u003cbr\u003e\n\tThe last part might be surprising if you aren't familiar with the PC-98 text\n\tchip. Contrary to the CGA and VGA text modes of IBM-compatibles, PC-98 text\n\tcells can only use a single color for \u003ci\u003eeither\u003c/i\u003e their foreground or\n\tbackground, with the other pixels being transparent and always revealing the\n\tpixels in VRAM below. If you look closely at the HUD in the images above,\n\tyou can see how the background of cells with gaiji glyphs is slightly\n\tbrighter (\u003ccode style=\"color: #100\"\u003e◼ #100\u003c/code\u003e) than the opaque black\n\tcells (\u003ccode style=\"color: #000\"\u003e◼ #000\u003c/code\u003e) surrounding them. This\n\trather custom color clearly implies that those pixels must have been\n\trendered by the graphics GDC. If any other sprite was rendered below the\n\tHUD, you would equally see it below the glyphs.\n\u003c/p\u003e\u003cp\u003e\n\tSo in the end, I did find the clear and logical system I was looking for,\n\tand managed to reduce the new clipping conditions down to \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/48c18d6dac613f1478bc5873087285f17ebf5836/th02/main/playfld.hpp#L81-L120\"\u003ea\n\tset of basic rules for each edge\u003c/a\u003e. Unfortunately, we also need a second\n\tmacro for each edge to differentiate between sprites that are smaller or\n\tlarger than the playfield border, which is treated as either 32×32 (for\n\t\u003ccode\u003esuper_roll_*()\u003c/code\u003e) or 32×16 (for non-\"rolling\"\n\t\u003ccode\u003esuper_*()\u003c/code\u003e functions). Since smaller sprites can be fully\n\tcontained within this border, the games can stop rendering them as soon as\n\ttheir bottom-right coordinate is no longer seen within the playfield, by\n\tcomparing against the clipping boundaries with \u003ccode\u003e\u0026lt;=\u003c/code\u003e and\n\t\u003ccode\u003e\u0026gt;=\u003c/code\u003e. For example, a 16×16 sprite would be completely\n\tinvisible once it reaches (﻿16,\u0026nbsp;0﻿), so it would still be rendered at\n\t(﻿17,\u0026nbsp;1﻿). A larger sprite during the scrolling part of a stage, like,\n\tsay, the 64×64 midbosses, would still be rendered if their top-left\n\tcoordinate was (﻿0,\u0026nbsp;-16﻿), so ZUN used \u003ccode\u003e\u0026lt;\u003c/code\u003e and\n\t\u003ccode\u003e\u0026gt;\u003c/code\u003e comparisons to at least get an additional pixel before\n\thaving to stop rendering such a sprite. Turbo C++ 4.0J sadly can't\n\tconstant-fold away such a difference in comparison operators.\n\u003c/p\u003e\u003cp\u003e\n\tAnd for the most part, ZUN did follow this system consistently. Except for,\n\tof course, the typical mistakes you make when faced with such manual\n\tdecisions, like how he treated TH04's Stage 4 midboss as a \"small\" sprite\n\tbelow 32×32 pixels (it's 64×64), losing that precious one extra pixel. Or\n\thow the entire rendering code for the 48×48 boss explosion sprite pretends\n\tthat it's actually 64×64 pixels large, which causes even the initial\n\ttransformation into screen space to be misaligned from the get-go.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e But these are additional bugs on top of the single\n\tone that led to all this research.\u003cbr\u003e\n\tBecause that's what this is, a bug. 🐞 Every resulting pixel boundary is a\n\tsystematic result of master.lib's unfortunate lack of clipping. It's as much\n\tof a bug as TH01's byte-aligned rendering of entities whose internal\n\tposition is not byte-aligned. In both cases, the entities are alive,\n\tsimulated, and partake in collision detection, but their rendered appearance\n\tdoesn't accurately reflect their internal position.\u003cbr\u003e\n\tInitially, I classified\n\t\u003ca href=\"/blog/2022-11-30\"\u003e📝 the sudden pop-in of TH05's Stage 5 midboss\u003c/a\u003e\n\tas a quirk because we had no conclusive evidence that this wasn't\n\tintentional, but now we do. There have been multiple explanations for why\n\tZUN put borders around the playfield, but master.lib's lack of sprite\n\tclipping might be the biggest reason.\n\u003c/p\u003e\u003cp\u003e\n\tAnd just like byte-aligned rendering, the clipping conditions can easily be\n\tremoved when porting the game away from PC-98 hardware. That's also what\n\tuth05win chose to do: By using OpenGL and not having to rely on hardware\n\tscrolling, it can simply place every sprite as a textured quad at its exact\n\tposition in screen space, and then draw the black playfield borders on top\n\tin the end to clip everything in a single draw call. This way, the Stage 5\n\tmidboss can smoothly fly into the playfield, just as defined by its movement\n\tcode:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-06-30-uth05win-Stage-5-midboss-entrance.webp?2c7d671d\" preload=\"none\" controls data-title=\"Playfield borders shown\" loop data-active width=\"640\" height=\"480\" data-fps=\"56\" data-frame-count=\"212\" style=\"aspect-ratio: 640 / 480\" data-lossless=\"/blog/static/video/zmbv/2023-06-30-uth05win-Stage-5-midboss-entrance.avi?50bb1a7b\"\u003e\u003csource src=\"/blog/static/video/av1/2023-06-30-uth05win-Stage-5-midboss-entrance.webm?f54e43bb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-06-30-uth05win-Stage-5-midboss-entrance.webm?c1e28ee1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-06-30-uth05win-Stage-5-midboss-entrance.webm?ce2a0dd3\" type=\"video/webm\"\u003eVideo of the TH05 Stage 5 midboss entrance animation in the uth05win port, featuring a sprite that smoothly enters the playfield from the top without the premature clipping seen in the PC-98 original. \u003ca href=\"/blog/static/video/zmbv/2023-06-30-uth05win-Stage-5-midboss-entrance.avi?50bb1a7b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.webp?db76732c\" preload=\"none\" controls data-title=\"Playfield borders hidden\" loop width=\"640\" height=\"480\" data-fps=\"56\" data-frame-count=\"212\" style=\"aspect-ratio: 640 / 480\" data-lossless=\"/blog/static/video/zmbv/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.avi?47279563\"\u003e\u003csource src=\"/blog/static/video/av1/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.webm?d26b17d1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.webm?4f8bab3b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.webm?c2cb7b38\" type=\"video/webm\"\u003eVideo of the TH05 Stage 5 midboss entrance animation in the uth05win port, with the covering black playfield borders disabled to reveal the lack of unnecessary clipping. \u003ca href=\"/blog/static/video/zmbv/2023-06-30-uth05win-Stage-5-midboss-entrance-no-borders.avi?47279563\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe entire smooth Stage 5 midboss entrance animation as shown in\n\t\tuth05win. If the simultaneous appearance of the \u003ci\u003eEnemy!!\u003c/i\u003e label\n\t\tdoesn't lend further proof to this having been ZUN's actual intention, I\n\t\tdon't know what will.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tMeanwhile, I designed the interface of the \u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 generic blitter used in the TH01 Anniversary Edition\u003c/a\u003e entirely around\n\tclipping the blitted sprite at any explicit combination of VRAM edges. This\n\twas nothing I tacked on in the end, but a core aspect that informed the\n\tarchitecture of the code from the very beginning. You \u003ci\u003ereally\u003c/i\u003e want to\n\thave one and \u003ci\u003eonly\u003c/i\u003e one place where sprite clipping is done right – and\n\tonly once per sprite, regardless of how many bitplanes you want to write to.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhich brings us to the goal that the final ¼ of this push went toward. I\n\tthought I was going to start cleaning up the\n\t\u003ca href=\"/blog/2021-10-20\"\u003e📝 player movement and rendering code\u003c/a\u003e, but\n\tthat turned out too complicated for that amount of time – especially if you\n\twant to start with \u003ci\u003ejust\u003c/i\u003e cleanup, preserving all original bugs for the\n\ttime being.\u003cbr\u003e\n\tFixing and smoothening player and Orb movement would be the next big task in\n\tAnniversary Edition development, needing about 3 pushes. It would start with\n\tmore performance research into runtime-shifting of larger sprites, followed\n\tby extending my generic blitter according to the results, writing new\n\toptimized loaders for the original image formats, and finally rewriting all\n\trendering code accordingly. With that code in place, we can then start\n\tcleaning up and fixing the unique code for each boss, one by one.\n\u003c/p\u003e\u003cp\u003e\n\tUntil that's funded, the code still contains a few smaller and easier pieces\n\tof code that are equally related to rendering bugs, but could be dealt with\n\tin a more incremental way. Line rendering is one of those, and first needs\n\tsome refactoring of every call site, including\n\t\u003ca href=\"/blog/2022-07-17\"\u003e📝 the rotating squares around Mima\u003c/a\u003e and\n\t\u003ca href=\"/blog/2022-08-08\"\u003e📝 YuugenMagan's pentagram\u003c/a\u003e. So far, I managed\n\tto remove another 1,360 bytes from the binary within this final ¼ of a push,\n\tbut there's still quite a bit to do in that regard.\u003cbr\u003e\n\tThis is the perfect kind of feature for smaller (micro-)transactions. Which\n\tmeans that we've now got meaningful TH01 code cleanup and Anniversary\n\tEdition subtasks at every price range, no matter whether you want to invest\n\ta lot or just a little into this goal.\n\u003c/p\u003e\u003cp\u003e\n\tIf you \u003ci\u003ecan\u003c/i\u003e, because Ember2528 revealed the plan behind\n\this Shuusou Gyoku contributions: A full-on Linux port of the game, which\n\twill be receiving all the funding it needs to happen. 🐧 Next up, therefore:\n\tTurning this into my main project within ReC98 for the next couple of\n\tmonths, and getting started by shipping the long-awaited first step towards\n\tthat goal.\u003cbr\u003e\n\tI've raised the cap to avoid the potential of rounding errors, which might\n\tprevent the last needed Shuusou Gyoku push from being correctly funded. I\n\talready had to pick the larger one of the two pending TH02 transactions for\n\tthis push, because we would have mathematically ended up\n\t\u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e25500\u003c/sub\u003e short of a full push with the smaller\n\ttransaction. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e And if I'm already at it, I might\n\tas well free up enough capacity to potentially ship the complete OpenGL\n\tbackend in a single delivery, which is currently estimated to cost 7 pushes\n\tin total.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-07-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-06-30T23:52:16Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-06-13",
      "url": "https://rec98.nmlgc.net/blog/2023-06-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-06-13\"\u003e\u003ctime datetime=\"2023-06-13T11:07:44Z\"\u003e2023-06-13 11:07\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0244\"\u003eP0244\u003c/a\u003e\n\t\t\tTH04 PI/RE (Stage 4 carpet lighting effect + 100% PI)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ac33bd2...97f0c3b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/stage\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The main scrolling portions of gameplay in TH02, TH04, and TH05. Mostly rendered using tile maps.\"\u003estage\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuuka-6\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 6 boss.\"\u003eyuuka-6\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cstyle\u003e\n\t#carpet-2023-06-13 img {\n\t\twidth: 768px;\n\t}\n\n\ttable#power_2023-06-13,\n\ttable#power_2023-06-13 tbody tr td:nth-child(2) {\n\t\twidth: min-content;\n\t}\n\n\ttable#power_2023-06-13 tbody tr th:first-child {\n\t\tbackground-color: black;\n\t}\n\n\ttable#power_2023-06-13 thead {\n\t\twhite-space: nowrap;\n\t}\n\n\ttable#power_2023-06-13 tbody tr td:nth-child(3) {\n\t\ttext-align: center;\n\t\twidth: 100%;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\t🎉 After almost 3 years, TH04 finally caught up to TH05 and is now 100%\n\tposition-independent as well! 🎉\n\u003c/p\u003e\u003cp\u003e\n\tFor a refresher on what this means and does not mean, check the\n\tannouncements from back in 2019 and 2020 when we chased the goal for TH05's\n\t\u003ca href=\"/blog/2019-12-29\"\u003e📝 \u003ccode\u003eOP.EXE\u003c/code\u003e\u003c/a\u003e and\n\t\u003ca href=\"/blog/2020-09-17\"\u003e📝 the rest of the game\u003c/a\u003e. These also feature\n\tsome demo videos that show off the kind of mods you were able to efficiently\n\tcode back then. With the occasional reverse-engineering attention it\n\treceived over the years, TH04's code should now be slightly easier to work\n\twith than TH05's was back in the day. Although not by much – TH04 has\n\tremained relatively unpopular among backers, and only received more than the\n\tfunded attention because it shares most of its core code with the more\n\tpopular TH05. Which, coincidentally, ended up becoming\n\t\u003ca href=\"/blog/2023-05-29\"\u003e📝 the reason for getting this done now\u003c/a\u003e.\u003cbr\u003e\n\tNot that it matters a lot. Ever since we reached 100% PI for TH05, community\n\tand backer interest in position independence has dropped to near zero. We\n\tjust didn't end up seeing the expected large amount of community-made mods\n\tthat PI was meant to facilitate, and even the\n\t\u003ca href=\"/blog/2022-08-15\"\u003e📝 100% decompilation of TH01\u003c/a\u003e changed nothing\n\tabout that. But that's OK; after all, I do appreciate the business of\n\tcontinually getting commissioned for \u003ci\u003eall\u003c/i\u003e the\n\t\u003ca href=\"/blog/2022-03-05\"\u003e📝 large-scale mods\u003c/a\u003e. Not focusing on PI is\n\talso the correct choice for everyone who likes reading these blog posts, as\n\tit often means that I can't go that much into detail due to cutting corners\n\tand piling up technical debt left and right.\n\u003c/p\u003e\u003cp\u003e\n\tSurprisingly, this only took 1.25 pushes, almost twice as fast as expected.\n\tAs that's closer to 1 push than it is to 2, I'm OK with releasing it like\n\tthis – especially since it was originally meant to come out three days ago.\n\t🍋 Unfortunately, it was delayed thanks to \u003ca\n\thref=\"https://github.com/nmlgc/rec98.nmlgc.net/commit/931197a58dd0b62b16b1294b353c4c3b5aac9e22\"\u003esurprising\n\twebsite bugs\u003c/a\u003e and a certain piece of code that was way more difficult to\n\tdocument than it was to decompile… The next push will have slightly less\n\tcontent in exchange, though.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2023-05-29\"\u003e📝 P0240 and P0241\u003c/a\u003e already covered the final\n\tremaining structures, so I only needed to do some superficial RE to prove\n\tthe remaining numeric literals as either constants or memory addresses. For\n\texample, I initially thought I'd have to decompile the dissolve animations\n\tin the staff roll, but I only needed to identify a single function pointer\n\ttype to prove all false positives as screen coordinates there. Now, the TH04\n\tstaff roll would be another fast and cheap decompilation, similar to the\n\tcustom entity types of TH04. (And TH05 as well!)\n\u003c/p\u003e\u003cp\u003e\n\tThe one piece of code I \u003ci\u003edid\u003c/i\u003e have to decompile was Stage 4's carpet\n\tlighting animation, thanks to hex literals that were way too complicated to\n\tleave in ASM. And this one probably takes the crown for TH04's worst set of\n\tlandmines and bloat that still somehow results in no observable bugs or\n\tquirks.\u003cbr\u003e\n\tThis animation starts at frame 1664, roughly 29.5 seconds into the stage,\n\tand quickly turns the stage background into a repeated row of dark-red plaid\n\tcarpet tiles by moving out from the center of the playfield towards the\n\tedges. Afterward, the animation repeats with a brighter set of tiles that is\n\tthen used for the rest of the stage. As I explained\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 a while ago in the context of TH02\u003c/a\u003e, the\n\tstage tile and map formats in PC-98 Touhou can't express animations, so all\n\tof this needed to be hardcoded in the binary.\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher id=\"carpet-2023-06-13\"\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAAgAQMAAAAhYj2mAAAABlBMVEVlAAAAAAD9XicmAAAAT0lEQVR42mP4Dwan6iEYO/gXDMEgwAChzkIxDrAZimEa/v8/B8U4QDEUwzT8/3+yHoJxOGkyBFOigVQnkeppUoN1NB5G42E0HkbjYTDFAwAgub5MJE1lSQAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Light level 0\"\n\t\talt=\"A row of the carpet tiles from TH04's Stage 4, at the lowest light level\"\n\t\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAAgAgMAAABmwkd2AAAACVBMVEWKAABlAAAAAAAoIf34AAAA4klEQVR4nO1VMQrEIBBcUmydFOmDLzkh94CD6A/0IalSprW9KvjKQ2NgN3hFxNJimUZmmBlhwHvnzlNq30ZF8dEdAzJMvOC9td4bE4m/5+MLn90bOQZOa4E5OJLAUSIwIkfmQOtIvKaI1pKIJDKMnHcHVQVyHVSNKNdB1ZJzHVT9pqkD57YtnFIA+9wDxUe3DEjx4gVrhTBmmiLx5wUUn92MFANn4OYOliSwlAj0SJE50LrrIrFMEcmSiCRSDJx3B1UF8h3UiuhfBzVLzndQ65vSDtoetD1oe9D2oO1B2wMrxA9toETNSUdWqgAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Light level 1\"\n\t\talt=\"A row of the carpet tiles from TH04's Stage 4, at the medium light level\"\n\t\u003e\u003cimg\n\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAAgAgMAAABmwkd2AAAADFBMVEX/RUWKAABlAAAAAAB3PiuCAAABDElEQVR4nO2VwQmEMBBFc59TYC0oEAsQEqzEBjxtHXsS7cAcUolgBTbgYZbRBEbIJcFjFj7vIj+ZvIURiMdBmWdr929jObMySeCMvQLxPBHX9Sr+3R9H5qUFTuqkbvGYYAoHTCUHNMAZewWi94jLchWP4YnGkidSwEmd1P2c4NUDUg5efaKUg1clpxy8+jcNDo7rt23War3rj+bMipHASZ3ULM7TuXUdBmul3LtOcualBU7qpO7nBOa+TWReGuCMEwjvnVuWvr9uru7bRGbFKOCkTup+TqCCA1XiQAFn2oEJDkyJgwY40w664KArcdACZ9qBDg50iQMJnNGBqPug7oO6D+o+qPug7gPv3B9P8HKMKHLEfgAAAABJRU5ErkJggg==\"\n\t\tdata-title=\"Light level 2\"\n\t\talt=\"A row of the carpet tiles from TH04's Stage 4, at the highest light level\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tThe repeating 384×16 row of carpet tiles at the beginning of TH04's\n\t\tStage 4 in all three light levels, shown twice for better visibility.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd ZUN did start out making the right decision by only using fully-lit\n\tcarpet tiles for all tile sections defined in \u003ccode\u003eST03.MAP\u003c/code\u003e. This\n\tway, the animation can simply disable itself after it completed, letting the\n\trest of the stage render normally and use new tile sections that are only\n\tdefined for the final light level. This means that the \"initial\" dark\n\tversion of the carpet is as much a result of hardcoded tile manipulation as\n\tthe animation itself.\u003cbr\u003e\n\tBut then, ZUN proceeded to implement it all by directly manipulating the\n\tring buffer of on-screen tiles. This is the lowest level before the tiles\n\tare rendered, and rather detached from the defined content of the\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 .MAP tile sections\u003c/a\u003e. Which leads to a whole\n\tlot of problems:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tIf you decide to do this kind of tile ring modification, it should ideally\n\thappen at a very specific point: \u003ci\u003eafter\u003c/i\u003e scrolling in new tiles into\n\tthe ring buffer, but \u003ci\u003ebefore\u003c/i\u003e blitting any scrolled or invalidated\n\ttiles to VRAM based on the ring buffer. Which is not where ZUN chose to put\n\tit, as he placed the call to the stage-specific render function after both\n\tof those operations. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e By the time the function is\n\tcalled, the tile renderer has already blitted a few lines of the fully-lit\n\tcarpet tiles from the defined .MAP tile section, matching the scroll speed.\n\tFortunately, these are hidden behind the black TRAM cells above and below\n\tthe playfield…\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tStill, the code needs to get rid of them before they would become visible.\n\tZUN uses the regular tile invalidation function for this, which will only\n\tcause actual redraws on the next frame. Again, the tile rendering call has\n\talready happened by the time the Stage 4-specific rendering function gets\n\tcalled.\u003cbr\u003e\n\tBut wait, this game also flips VRAM pages between frames to provide a\n\ttear-free gameplay experience. This means that the intended redraw of the\n\tnew tiles actually hits the wrong VRAM page. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tAnd sure, the code does attempt to invalidate these newly blitted lines\n\tevery frame – but only relative to the current VRAM Y coordinate that\n\trepresents the top of the hardware-scrolled screen. Once we're back on the\n\toriginal VRAM page on the next frame, the lines we initially set out to\n\tremove could have already scrolled past that point, making it impossible to\n\tever catch up with them in this way.\u003cbr\u003e\n\tThe only real \"solution\": Defining the height of the tile invalidation\n\trectangle at 3× the scroll speed, which ensures that each invalidation call\n\tcovers 3 frames worth of newly scrolled-in lines. This is not intuitive at\n\tall, and requires an understanding of everything I have just written to even\n\tarrive at this conclusion. Needless to say that ZUN didn't comprehend it\n\teither, and just hardcoded an invalidation height that happened to be enough\n\tfor the small scroll speeds defined in \u003ccode\u003eST03.STD\u003c/code\u003e for the first\n\t30 seconds of the stage.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\n\tThe effect must consistently modify the tile ring buffer to \"fix\" any new\n\ttiles, overriding them with the intended light level. During the animation,\n\tthe code not only needs to set the old light level for any tiles that are\n\tstill waiting to be replaced, but also the new light level for any tiles\n\tthat \u003ci\u003ewere\u003c/i\u003e replaced – and ZUN forgot the second part. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e  As a result, newly scrolled-in tiles within the already animated\n\tarea will \"remain\" untouched at light level 2 if the scroll speed is fast\n\tenough during the transition from light level 0 to 1.\n\u003c/li\u003e\u003c/ol\u003e\u003cp\u003e\n\tAll that means that we only have to raise the scroll speed for the effect to\n\tfall apart. Let's try, say, 4 pixels per frame rather than the original\n\t0.25:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.webp?2c500a6c\" preload=\"none\" controls data-title=\"TRAM hidden\" loop data-active width=\"384\" height=\"400\" data-fps=\"14.108\" data-frame-count=\"120\" style=\"aspect-ratio: 384 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.avi?47d692b5\"\u003e\u003csource src=\"/blog/static/video/av1/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.webm?7879d976\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.webm?6253241c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.webm?3ea26cdc\" type=\"video/webm\"\u003eVideo of TH04's Stage 4 carpet lighting animation with an increased scroll speed of 4 pixels per frame and the text RAM layer hidden, highlighting all landmines in ZUN's original code. \u003ca href=\"/blog/static/video/zmbv/2023-06-13-TH04-Stage-4-carpet-lighting-landmines-no-TRAM.avi?47d692b5\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"30\" data-title=\"➜ 1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"62\" data-title=\"➜ 2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"86\" data-title=\"💡 2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.webp?8bad715a\" preload=\"none\" controls data-title=\"TRAM shown\" loop width=\"384\" height=\"400\" data-fps=\"14.108\" data-frame-count=\"120\" style=\"aspect-ratio: 384 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.avi?77a29bf6\"\u003e\u003csource src=\"/blog/static/video/av1/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.webm?df0badb1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.webm?3043ca0d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.webm?6695f031\" type=\"video/webm\"\u003eVideo of TH04's Stage 4 carpet lighting animation with an increased scroll speed of 4 pixels per frame and the text RAM layer shown. \u003ca href=\"/blog/static/video/zmbv/2023-06-13-TH04-Stage-4-carpet-lighting-landmines.avi?77a29bf6\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"30\" data-title=\"➜ 1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"62\" data-title=\"➜ 2\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"86\" data-title=\"💡 2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tBy hiding the text RAM layer and revealing what's below the usually\n\t\topaque black cells above and below the playfield, we can observe all\n\t\tthree landmines – 1) and 2) throughout light level 0, and 3) during the\n\t\ttransition from level 0 to 1.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAll of this could have been so much simpler and actually stable if ZUN\n\tapplied the tile changes directly onto the .MAP. This is a much more\n\tintuitive way of expressing what is supposed to happen to the map, and would\n\thave reduced the code to the actually necessary tile changes for the first\n\tframe and each individual frame of the animation. It would have still\n\trequired a way to force these changes into the tile ring buffer, but ZUN\n\tcould have just used his existing full-playfield redraw functions for that.\n\tIn any case, there would have been no need for \u003ci\u003eany\u003c/i\u003e per-frame tile\n\tfixing and redrawing. The CPU cycles saved this way could have then maybe\n\tbeen put towards writing the tile-replacing part of the animation in C++\n\trather than ASM…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWow, that was an unreasonable amount of research into a feature that\n\tsuperficially works fine, just because its decompiled code didn't make\n\tsense. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e To end on a more positive note, here are\n\tsome minor new discoveries that might actually matter to someone:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThe laser part of Marisa's \u003ci\u003eIllusion Laser\u003c/i\u003e shot type always does 3\n\tpoints of damage per frame, regardless of the player's power level. Its\n\thitbox also remains identical on all power levels, no matter how wide the\n\tlaser appears on screen. The strength difference between the levels purely\n\tcomes from the number of \u003ci\u003eframes\u003c/i\u003e the laser stays active before a fixed\n\tnon-damaging 32-frame cooldown time:\n\t\u003cfigure\u003e\u003ctable id=\"power_2023-06-13\" class=\"numbers\"\u003e\n\t\t\u003cthead\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003ePower level\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003eFrames per cycle (including 32-frame cooldown)\u003c/th\u003e\n\t\t\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #f00\"\u003e2\u003c/th\u003e\u003ctd\u003e64\u003c/td\u003e\u003ctd rowspan=\"8\"\u003e\u003cimg\n\t\t\t\tclass=\"inline_sprite\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAADgBAMAAABBW30DAAAAKlBMVEUAAAD/////7u7/7sz//1Xuu6qZmf+qqkTMVcz/RER3AHeIAAAAAN0AAACWojNmAAAAAXRSTlMAQObYZgAAAf5JREFUeNrt07GK20AQBuDhipBCKtz4AdQkjSuT8hqzIAgEAkGlwdeocmlYOFB9Rm9g8gAXBuYJ3LhytYUgMTJj5l3CrrQgG61NysBO+ftjmBmtAaAsyxJuC3wOEUQQQQQRRBBBBBFEEEEEEUQQQQQRRBBBBBFE8H8CEbkLnra/qLkHPteIEgKQQbpFRAqBeQbnGpGxGQfpfC5nRCIJADgVgogsaQikhTARNRACqYggcwOHUWB/Pj0LUZOsR8GMmeXHs0igw9OWiEh+f1nK+CU/bmu0PeTEgTW39oj2CkI0PiTXVtg23FwBY0q/JWPXhYb52phj/xS0J80wN2Vpug6t1lpcDfPk+DJdu8VTlWtb3ZY+B1NOJh2VTKmNzrX0jfvcvEwmUwf+ZJ+UUkq7HXw+BLLIN1mW5SzXwLVauRm+6cUiy3kGw9wNc5y6Lap2o5SWmR+yy906tgO0lbS5qvqP5fNVYoxJ+ktSm+cVuRl8/mEKxhwOdmFhYdFa3JA+B1hBkiSugxAJtZV9UzDM4dA1AGZG/yqHuZcghEjsvuZV7guY0V7S//duC0DeN18RUb2GAL/WNdd1/fZajYOWusdwfg91kGIpIsWSJTDD6Xt62QsU/a1vC9IC0t1eAAINnNntQr/24PIIXH42dwFc9g+AXB6AVB6B+zMCQPOv4C+KgKJn9nbbeQAAAABJRU5ErkJggg==\" alt=\"\"\n\t\t\t\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #f0f\"\u003e3\u003c/th\u003e\u003ctd\u003e72\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #f0f\"\u003e4\u003c/th\u003e\u003ctd\u003e88\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #00f\"\u003e5\u003c/th\u003e\u003ctd\u003e104\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #0f0\"\u003e6\u003c/th\u003e\u003ctd\u003e128\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #0ff\"\u003e7\u003c/th\u003e\u003ctd\u003e144\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #ff0\"\u003e8\u003c/th\u003e\u003ctd\u003e168\u003c/td\u003e\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\u003cth style=\"color: #fff\"\u003e9\u003c/th\u003e\u003ctd\u003e192\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003c/tbody\u003e\n\t\u003c/table\u003e\u003c/figure\u003e\n\u003c/li\u003e\u003cli\u003e\n\tThe decay animation for player shots is faster in TH05 (12 frames) than in\n\tTH04 (16 frames).\n\u003c/li\u003e\u003cli\u003e\n\tIn the first phase of her Stage 6 fight, Yuuka moves along one of two\n\trandomly chosen hardcoded paths, defined as a set of 5 movement angles.\n\tAfter reaching the final point and firing a danmaku pattern, she teleports\n\tback to her initial position to repeat the path one more time before the\n\tphase times out.\n\u003c/li\u003e\u003cli\u003e\n\tSimilarly, TH04's Stage 3 midboss also goes through 12 fixed movement angles\n\tbefore flying off the playfield.\n\u003c/li\u003e\u003cli\u003e\n\tThe formulas for calculating the skill rating on both TH04's and TH05's\n\tfinal verdict screen are going to be \u003ci\u003every\u003c/i\u003e long and complicated.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up: ¾ of a push filled with random boilerplate, finalization, and TH01\n\tcode cleanup work, while I finish the preparations for Shuusou Gyoku's\n\tOpenGL backend. This month, everything should finally work out as intended:\n\tI'll complete both tasks in parallel, ship the former to free up the cap,\n\tand then ship the latter once its 5\u003csup\u003eth\u003c/sup\u003e push is fully funded.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-06-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-06-13T11:07:44Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-06-07",
      "url": "https://rec98.nmlgc.net/blog/2023-06-07",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-05-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-06-07\"\u003e\u003ctime datetime=\"2023-06-07T23:17:58Z\"\u003e2023-06-07 23:17\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0242\"\u003eP0242\u003c/a\u003e\n\t\t\tTH02 RE (Score tracking + HUD rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/08352a5...dfa758d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0243\"\u003eP0243\u003c/a\u003e\n\t\t\tTH02 RE (Items)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dfa758d...ac33bd2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/item\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Collectables, dropped from enemies and (mid)bosses.\"\u003eitem\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/enemy\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Common stage enemies with simple scripts, in contrast to midbosses or bosses.\"\u003eenemy\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/stones\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH02\u0026#39;s Stage 3 boss. Arguably the best Touhou character.\"\u003estones\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#points-2023-06-07,\n\t#points-2023-06-07 \u003e img {\n\t\twidth: 768px;\n\t}\n\t#collect_skill_2023-06-07 .item_row,\n\t#collect_skill_2023-06-07 th {\n\t\tborder-bottom: var(--table-border);\n\t}\n\t#collect_skill_2023-06-07 tbody td,\n\t#collect_skill_2023-06-07 tbody th {\n\t\ttext-align: right;\n\t}\n\t#collect_skill_2023-06-07 tbody td:last-child {\n\t\ttext-align: left;\n\t}\n\u003c/style\u003e\u003cp\u003e\n\tOK, let's decompile TH02's HUD code first, gain a solid understanding of how\n\tincreasing the score works, and then look at the item system of this game.\n\tShould be no big deal, no surprises expected, let's go!\n\u003c/p\u003e\u003cp\u003e\n\t…Yeah, right, that's \u003ci\u003enever\u003c/i\u003e how things end up in ReC98 land.\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e And so, we get the usual host of newly discovered\n\toddities in addition to the expected insights into the item mechanics. Let's\n\tstart with the latter:\n\u003cp\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\tSome regular stage enemies appear to randomly drop either \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e or \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e items. In reality, there is\n\t\tvery little randomness at play here: These items are picked from a\n\t\thardcoded, repeating ring of 10 items\n\t\t(𝄆\u0026nbsp;\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u0026nbsp;𝄇), and the only source of\n\t\trandomness is the initial position within this ring, which changes at\n\t\tthe beginning of every stage. ZUN further increased the illusion of\n\t\trandomness by only dropping such a semi-random item for every\n\t\t3\u003csup\u003erd\u003c/sup\u003e defeated enemy that is coded to drop one, and also having\n\t\tenemies that drop fixed, non-random items. I'd say it's a decent way of\n\t\tensuring both randomness and balance.\u003cul\u003e\u003cli\u003e\n\t\t\tThere's a \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e512\u003c/sub\u003e chance for such a semi-random\n\t\t\titem drop to turn into a \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Bomb\" title=\"Bomb\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAFWIEf///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI5nIOJxqffBhK00uWuqngKDwaAFGjmFwxAmHAieYZq6KIjjWpzibbaCLO4RsDHg9gwIpARpDPCeEYKADs=\"\n\t\u003e item instead –\n\t\t\twhich translates to \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e1536\u003c/sub\u003e enemies due to the\n\t\t\tfixed drop rate.\n\t\t\u003c/li\u003e\u003cli\u003e\n\t\t\t\u003cstrong\u003eEdit (2023-06-11):\u003c/strong\u003e These are the only ways that items can randomly drop in this game. All other drops, including\n\t\t\tany \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"1-up\" title=\"1-up\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKECAIgAiP///wAAAAAAACH5BAEKAAIALAAAAAAQABAAAAIzlA2pcA0BI2RO2kDFw0tNPXVLOEUiyYVjyp7t9mwmDGM2mpQyLtEf7crsLhnE5dNAiIoFADs=\"\n\t\u003e items, are scripted and deterministic.\n\t\t\u003c/li\u003e\u003cli\u003e\n\t\t\tAfter using a continue (both after a Game Over, or after manually\n\t\t\tchoosing to do so through the Pause menu for whatever reason), the\n\t\t\tnext\n\t\t\u003ccode\u003e(﻿\u003cvar\u003eStage number\u003c/var\u003e\u0026nbsp;+\u0026nbsp;1)\u003c/code\u003e semi-random item\n\t\tdrops are turned into \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"5-power\"\n\ttitle=\"5-power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPEDAP////8AAAAAAAAAACH5BAUKAAMALAAAAAAQABAAAAI2nAepeQ0BIwQNyRuoeXmpaXWLOHEJdnZTmZomO6qy1LreKttvjuWh98kcOD2hYFikHB1ApaEAADs=\"\n\t\u003e items instead.\n\t\t\u003c/li\u003e\u003c/ul\u003e\n\t\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eItems can contribute up to 25 points to the skill value and subsequent\n\t\trating (\u003cspan lang=\"ja\"\u003eあなたの腕前\u003c/span\u003e) on the final verdict\n\t\tscreen. Doing well at item collection first increases a separate\n\t\t\u003ccode\u003ecollect_skill\u003c/code\u003e value:\u003c/p\u003e\n\t\t\u003cfigure\u003e\u003ctable id=\"collect_skill_2023-06-07\"\u003e\n\t\t\t\u003cthead\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eItem\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCollection condition\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003ecollect_skill\u003c/code\u003e change\u003c/th\u003e\n\t\t\t\t\u003c/tr\u003e\n\t\t\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth rowspan=\"2\"\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e\u003c/th\u003e\n\t\t\t\t\t\u003ctd\u003ebelow max power\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+1\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr class=\"item_row\"\u003e\n\t\t\t\t\t\u003ctd\u003eat or above max power\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+2\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003cth rowspan=\"4\"\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e\u003c/th\u003e\n\t\t\t\t\t\u003ctd\u003evalue == 51,200\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+8\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003evalue ≥20,000 and \u0026lt;51,200\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+4\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003evalue ≥10,000 and \u0026lt;20,000\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+2\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr class=\"item_row\"\u003e\n\t\t\t\t\t\u003ctd\u003evalue \u0026lt;10,000\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+1\u003c/td\u003e\n\t\t\t\t\u003c/tr\u003e\u003ctr class=\"item_row\"\u003e\n\t\t\t\t\t\u003cth\u003e\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Bomb\" title=\"Bomb\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAFWIEf///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI5nIOJxqffBhK00uWuqngKDwaAFGjmFwxAmHAieYZq6KIjjWpzibbaCLO4RsDHg9gwIpARpDPCeEYKADs=\"\n\t\u003e\u003c/th\u003e\n\t\t\t\t\t\u003ctd\u003ewith 5 bombs in stock\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e+16\u003c/td\u003e\n\t\t\t\u003c/tbody\u003e\n\t\t\u003c/table\u003e\u003cfigcaption\u003e\n\t\t\tNote, again, the lack of anything involving \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"1-up\" title=\"1-up\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKECAIgAiP///wAAAAAAACH5BAEKAAIALAAAAAAQABAAAAIzlA2pcA0BI2RO2kDFw0tNPXVLOEUiyYVjyp7t9mwmDGM2mpQyLtEf7crsLhnE5dNAiIoFADs=\"\n\t\u003e\n\t\t\titems. At the maximum of 5 lives, the item spawn function transforms\n\t\t\tthem into bomb items anyway. It \u003ci\u003eis\u003c/i\u003e possible though to gain\n\t\t\tthe 5\u003csup\u003eth\u003c/sup\u003e life by reaching one of the extend scores while a\n\t\t\t\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"1-up\" title=\"1-up\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKECAIgAiP///wAAAAAAACH5BAEKAAIALAAAAAAQABAAAAIzlA2pcA0BI2RO2kDFw0tNPXVLOEUiyYVjyp7t9mwmDGM2mpQyLtEf7crsLhnE5dNAiIoFADs=\"\n\t\u003e item is still on screen; in that case,\n\t\t\tcollecting the 1-up has no effect at all.\n\t\t\u003c/figcaption\u003e\u003c/figure\u003e\n\t\t\u003cp\u003eEvery 32 \u003ccode\u003ecollect_skill\u003c/code\u003e points will then raise the\n\t\t\u003ccode\u003eitem_skill\u003c/code\u003e by 1, whereas every 16 dropped items will lower\n\t\tit by 1. Before launching into the ending sequence,\n\t\t\u003ccode\u003eitem_skill\u003c/code\u003e is clamped to the [﻿0;\u0026nbsp;25﻿] range and\n\t\tadded to the other skill-relevant metrics we're going to look at in\n\t\tfuture pushes.\u003c/p\u003e\n\t\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\tWhen losing a life, the game will drop a single\n\t\t\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"5-power\"\n\ttitle=\"5-power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPEDAP////8AAAAAAAAAACH5BAUKAAMALAAAAAAQABAAAAI2nAepeQ0BIwQNyRuoeXmpaXWLOHEJdnZTmZomO6qy1LreKttvjuWh98kcOD2hYFikHB1ApaEAADs=\"\n\t\u003e and 4 randomly picked \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e or \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e items in a random order\n\t\taround Reimu's position. Contrary to \u003ca\n\t\thref=\"https://en.touhouwiki.net/index.php?title=Story_of_Eastern_Wonderland%2FGameplay\u0026type=revision\u0026diff=55912\u0026oldid=55911\"\u003ean\n\t\tunsourced Touhou Wiki edit from 2009\u003c/a\u003e, each of the 4 does have \u003cspan\n\t\tclass=\"hovertext\" title=\"Sure, under the assumption that the master.lib RNG is uniformly distributed for every seed, but the intent of ZUN's code is clear in this regard.\"\u003ean\n\t\tequal and independent chance\u003c/span\u003e of being either a\n\t\t\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Power\" title=\"Power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAP8AAP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI8nIOJxqffBhK00uWsFni+yQWAFFylGQ4ACIbhyD6mGm8IfLbVspaKdstsXirHJzhiHEXFyOhZTEaiVEYBADs=\"\n\t\u003e or \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e item.\n\t\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\n\t\t\u003cp\u003eFinally, and perhaps \u003ca\n\t\thref=\"https://twitter.com/jazz_cappricio/status/1665508116799053824\"\u003emost\n\t\tinterestingly\u003c/a\u003e, \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e item values! These are\n\t\tdetermined by the top Y coordinate of an item during the frame it is\n\t\tcollected on. The maximum value of 51,200 points applies to the top 48\n\t\tpixels of the playfield, and drops off as soon as an item falls below\n\t\tthat line. For the rest of the playfield, point items then use a formula\n\t\tof \u003ccode\u003e(﻿28,000\u0026nbsp;-\u0026nbsp;(﻿\u003cvar\u003etop Y coordinate of item in\n\t\tscreen space\u003c/var\u003e\u0026nbsp;×\u0026nbsp;70﻿)﻿)\u003c/code\u003e:\u003c/p\u003e\n\t\t\u003cfigure class=\"pixelated\" id=\"points-2023-06-07\"\u003e\n\t\t\t\u003cimg src=\"/blog/static/2023-06-07-TH02-Point-item-value.png?a719d9f3\" alt=\"\"\u003e\u003cfigcaption\u003e\n\t\t\tPoint items and their collection value in TH02. The numbers\n\t\t\tcorrespond to items that are collected while their top Y coordinate\n\t\t\tmatches the line they are directly placed on. The upper\n\t\t\t\u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"Point\" title=\"Point\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAKEDAAAAAIgAiP///2ZmZiH5BAEKAAMALAAAAAAQABAAAAI+nIOJxqffBhK00uXsrHjf9AFSoJFcMADTAyLiagkrqpLs/JrxmW62DHQ5YCahaHQLiI7DxzKSij6ZkWmUUQAAOw==\"\n\t\u003e item in the image would therefore give\n\t\t\t23,450 points if the player collected it at that specific\n\t\t\tposition.\u003cbr\u003e\n\t\t\tReimu collects any item whose 16×16 bounding box lies fully within\n\t\t\tthe \u003cspan style=\"color: red;\"\u003ered\u003c/span\u003e 48×40 hitbox. Note that\n\t\t\tthe box isn't cut off in this specific case: At Reimu's lowest\n\t\t\tpossible position on the playfield, the lowest 8 pixels of her\n\t\t\tsprite are clipped, but the item hitbox still happens to end exactly\n\t\t\tat the bottom of the playfield. Since an item's Y velocity\n\t\t\taccelerates on every frame, it's entirely possible to collect a\n\t\t\tpoint item at the lowest value of 2,240 points, on the exact frame\n\t\t\tbefore it falls below the collection hitbox.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tOnto score tracking then, which only took a single commit to raise another\n\tbig research question. It's widely known that TH02 grants extra lives upon\n\treaching a score of 1, 2, 3, 5, or 8 million points. But what hasn't been\n\tdocumented is the fact that the game does not stop at the end of the\n\thardcoded extend score array. ZUN merely ends it with a sentinel value of\n\t999,999,990 points, but if the score ever increased beyond this value, the\n\tgame will interpret adjacent memory as signed 32-bit score values and\n\tcontinue giving out extra lives based on whatever thresholds it ends up\n\tfinding there. Since the following bytes happen to turn into a negative\n\tnumber, the next extra life would be awarded right after gaining another 10\n\tpoints at exactly 1,000,000,000 points, and the threshold after that would\n\tbe 11,114,905,600 points. Without an explicit counterstop, the number of\n\tscore-based extra lives is theoretically unlimited, and would even continue\n\tafter the signed 32-bit value overflowed into the negative range. Although\n\twe certainly have bigger problems once scores ever reach that point…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThat said, it seems impossible that any of this could ever happen\n\tlegitimately. The current high scores of \u003ca\n\thref=\"https://www.youtube.com/watch?v=Q-XY9bb50_k\"\u003e42,942,800 points on\n\tLunatic\u003c/a\u003e and \u003ca\n\thref=\"https://www.youtube.com/watch?v=nUmMpA1f-Kc\"\u003e42,603,800 points on\n\tExtra\u003c/a\u003e don't even reach \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e20\u003c/sub\u003e of ZUN's sentinel\n\tvalue. Without either a graze or a bullet cancel system, the scoring\n\tpotential in this game is fairly limited, making it unlikely for high scores\n\tto ever increase by that additional order of magnitude to end up anywhere\n\tnear the 1 billion mark.\u003cbr\u003e\n\tBut can we \u003ci\u003ereally\u003c/i\u003e be sure? Is this a landmine because it's impossible\n\tto ever reach such high scores, or is it a quirk because these extends\n\t\u003ci\u003ecould\u003c/i\u003e be observed under rare conditions, perhaps as the result of\n\tother quirks? And if it's the latter, how many of these adjacent bytes do we\n\tneed to preserve in cleaned-up versions and ports? We'd pretty much need to\n\tknow the upper bound of high scores within the original stage and boss\n\tscripts to tell. This value \u003ci\u003eshould\u003c/i\u003e be rather easy to calculate in a\n\tgame with such a simple scoring system, but doing that only makes sense\n\tafter we RE'd all scoring-related code and could efficiently run such\n\tsimulations. It's definitely something we'd need to look at before working\n\ton this game's \u003ccode\u003edebloated\u003c/code\u003e version in the far future, which is\n\twhen the difference between quirks and landmines will become relevant.\n\tStill, all that uncertainty just because ZUN didn't restrict a loop to the\n\tsize of the extend threshold array…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tTH02 marks a pivotal point in how the PC-98 Touhou games handle the current\n\tscore. It's the last game to use a 32-bit variable before the later games\n\twould regrettably start using arrays of \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\"\u003ebinary-coded\n\tdecimals\u003c/a\u003e. More importantly though, TH02 is also the first game to\n\tintroduce the delayed score counting animation, where the displayed score\n\tintentionally lags behind and gradually counts towards the real one over\n\tmultiple frames. This could be implemented in one of two ways:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eKeep the displayed score as a separate variable inside the presentation\n\tlayer, and let it gradually count up to the real score value passed in from\n\tthe logic layer\u003c/li\u003e\n\t\u003cli\u003eBurden the game logic with this presentation detail, and split the score\n\tinto two variables: One for the displayed score, and another for the\n\t\u003ci\u003edelta\u003c/i\u003e between that score and the actual one. Newly gained points are\n\tfirst added to the delta variable, and then gradually subtracted from there\n\tand added to the real score before being displayed.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tAnd by now, we can all tell which option ZUN picked for the rest of the\n\tPC-98 games, even if you don't remember\n\t\u003ca href=\"/blog/2022-03-27\"\u003e📝 me mentioning this system last year\u003c/a\u003e.\n\t\u003ca href=\"/blog/2023-03-30\"\u003e📝 Once again\u003c/a\u003e, TH02 immortalized ZUN's initial\n\tattempt at the concept, which lacks the abstraction boundaries you'd want\n\tfor managing this one piece of state across two variables, and messes up the\n\tabstractions it \u003ci\u003edoes\u003c/i\u003e have. In addition to the regular score\n\ttransfer/render function, the codebase therefore has\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ea function that transfers the current delta to the score immediately,\n\tbut does not re-render the HUD, and\u003c/li\u003e\n\t\u003cli\u003ea function that adds the delta to the score and re-renders the HUD, but\n\tdoes not reset the delta.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd – you guessed it – I wouldn't have mentioned any of this if it didn't\n\tresult in one bug and one quirk in TH02. The bug resulting from 1) is pretty\n\tminor: The function is called when losing a life, and simply stops any\n\tactive score-counting animation at the value rendered on the frame where the\n\tplayer got hit. This one is only a rendering issue – no points are lost, and\n\tyou just need to gain 10 more for the rendered value to jump back up to its\n\tactual value. You'll probably never notice this one because you're likely\n\tbusy collecting the single \u003cimg class=\"inline_sprite\" width=\"16\" height=\"16\" alt=\"5-power\"\n\ttitle=\"5-power\"\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPEDAP////8AAAAAAAAAACH5BAUKAAMALAAAAAAQABAAAAI2nAepeQ0BIwQNyRuoeXmpaXWLOHEJdnZTmZomO6qy1LreKttvjuWh98kcOD2hYFikHB1ApaEAADs=\"\n\t\u003e spawned around Reimu\n\twhen losing a life, which always awards at least 10 points.\n\u003c/p\u003e\u003cp\u003e\n\tThe quirk resulting from 2) is more intriguing though. Without a separate\n\treset of the score delta, the function effectively awards the current delta\n\tvalue as a one-time point bonus, since the same delta will still be\n\tregularly transferred to the score on further game frames.\u003cbr\u003e\n\tThis function is called at the start of every dialog sequence. However, TH02\n\tstops running the regular game loop between the post-boss dialog and the\n\tnext stage where the delta is reset, so we can only observe this quirk for\n\tthe pre-boss sequences and the dialog before Mima's form change.\n\tUnfortunately, it's not all too exploitable in either case: Each of the\n\tpre-boss dialog sequences is preceded by an ungrazeable pellet pattern and\n\tfollowed by multiple seconds of flying over an empty playfield with zero\n\tscoring opportunities. By the time the sequence starts, the game will have\n\tlong transferred any big score delta from max-valued point items. It's\n\tslightly better with Mima since you can at least shoot her and use a bomb to\n\tkeep the delta at a nonzero value, but without a health bar, there is little\n\tindication of \u003ci\u003ewhen\u003c/i\u003e the dialog starts, and it'd be long after Mima\n\tgave out her last bonus items in any case.\u003cbr\u003e\n\tBut two of the bosses – that is, Rika, and the Five Magic Stones – are\n\tscrolled onto the playfield as part of the stage script, and can also be hit\n\twith player shots and bombs for a few seconds before their dialog starts.\n\tWhile I'll only get to cover shot types and bomb damage within the next few\n\tTH02 pushes, there is an obvious initial strategy for maximizing the effect\n\tof this quirk: Spreading out the A-Type / Wide / High Mobility shot to land\n\tas many hits as possible on all Five Magic Stones, while firing off a bomb.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-06-07-TH02-Score-delta-dialog-start-quirk.webp?3c58767c\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"300\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-06-07-TH02-Score-delta-dialog-start-quirk.avi?cff1a7ae\"\u003e\u003csource src=\"/blog/static/video/av1/2023-06-07-TH02-Score-delta-dialog-start-quirk.webm?991461fe\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-06-07-TH02-Score-delta-dialog-start-quirk.webm?877bd600\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-06-07-TH02-Score-delta-dialog-start-quirk.webm?3379b099\" type=\"video/webm\"\u003eVideo of an attempt to maximize the effects of TH02's score-delta-as-bonus quirk when starting a boss fight, by trying to land as many A-Type / Wide / High Mobility shots as possible on all Five Magic Stones while bombing. \u003ca href=\"/blog/static/video/zmbv/2023-06-07-TH02-Score-delta-dialog-start-quirk.avi?cff1a7ae\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"223\" data-title=\"Last frame of stage portion\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"224\" data-title=\"1750 extra points added\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003cfigcaption\u003e\n\tTurns out that the infamous button-mashing mechanics of the\n\tplayer shot are also more complicated than simply pressing and releasing the\n\tShot key at alternating frames. Even this result took way too many\n\ttakes.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWow, a grand total of 1,750 extra points! Totally worth wasting a bomb for…\n\tyeah, probably not. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e But at the very least, it's\n\tsomething that a TAS score run would want to keep in mind. And all that just\n\tbecause ZUN \"forgot\" a single \u003ccode\u003escore_delta = 0;\u003c/code\u003e assignment at\n\tthe end of one function…\n\u003c/p\u003e\u003cp\u003e\n\tAnd that brings TH02 over the 30% RE mark! Next up: 100% position\n\tindependence for TH04. If anyone wants to grab the \u003cscript\u003eformatCurrency(1033)\u003c/script\u003e\u003cnoscript\u003e10.33\u0026nbsp;€\u003c/noscript\u003e\n\tthat have now been freed up in the cap: Any small Touhou-related task would\n\tbe perfect to round out that upcoming TH04 PI delivery.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-05-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-06-07T23:17:58Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-05-29",
      "url": "https://rec98.nmlgc.net/blog/2023-05-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-05-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-05-29\"\u003e\u003ctime datetime=\"2023-05-29T23:39:52Z\"\u003e2023-05-29 23:39\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0240\"\u003eP0240\u003c/a\u003e\n\t\t\tTH04 PI/RE (Stage 5 star rendering + Stage 6 Yuuka checkerboard + Custom entity structures, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/be69ab6...40c900f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0241\"\u003eP0241\u003c/a\u003e\n\t\t\tTH04 PI/RE (Custom entity structures, part 2/2 + Thick laser structure + PI false positives + .STD loading)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/40c900f...08352a5\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eJonathKane, Blue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kurumi\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 2 boss.\"\u003ekurumi\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/reimu-4\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 4 boss, when playing as Marisa.\"\u003ereimu-4\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/marisa-4\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 4 boss, when playing as Reimu.\"\u003emarisa-4\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuuka-5\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 5 boss.\"\u003eyuuka-5\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuuka-6\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 6 boss.\"\u003eyuuka-6\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gengetsu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s second Extra Stage boss.\"\u003egengetsu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cp\u003e\n\tWell, well. My original plan was to ship the first step of Shuusou Gyoku\n\tOpenGL support on the next day after this delivery. But unfortunately, the\n\tcomplications just kept piling up, to a point where the required solutions\n\tdefinitely blow the current budget for that goal. I'm currently sitting on\n\tover 70 commits that would take at least 5 pushes to deliver as a meaningful\n\trelease, and all of that is just \u003ci\u003erearchitecting\u003c/i\u003e work, preparing the\n\tgame for a not too Windows-specific OpenGL backend in the first place. I\n\thaven't even \u003ci\u003ewritten\u003c/i\u003e a single line of OpenGL yet… 🥲\u003cbr\u003e\n\tThis shifts the intended Big Release Month™ to June after all. Now I know\n\tthat the next round of Shuusou Gyoku features should better start with the\n\tSC-88Pro recordings, which are much more likely to get done within their\n\tcurrent budget. At least I've already completed the configuration versioning\n\tsystem required for that goal, which leaves only the actual audio part.\n\u003c/p\u003e\u003cp\u003e\n\tSo, TH04 position independence. Thanks to a bit of funding for stage\n\tdialogue RE, non-ASCII translations will soon become viable, which finally\n\tpresents a reason to push TH04 to 100% position independence after\n\t\u003ca href=\"/blog/2020-09-17\"\u003e📝 TH05 had been there for almost 3 years\u003c/a\u003e. I\n\thaven't heard back from \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e about how much they want to be\n\tinvolved in funding this goal, if at all, but maybe other backers are\n\tinterested as well.\u003cbr\u003e\n\tAnd sure, it would be entirely possible to implement non-ASCII translations\n\tin a way that retains the layout of the original binaries and can be easily\n\tcompared at a binary level, in case we consider translations to be a\n\tcritical piece of infrastructure. This wouldn't even just be an exercise in\n\tneedless perfectionism, and we only have to look to Shuusou Gyoku to realize\n\twhy: \u003ca href=\"https://www.youtube.com/watch?v=S5QFqA5fZ48\"\u003ePlayers expected\n\tthat my builds were compatible with existing SpoilerAL SSG files\u003c/a\u003e, which\n\twas something I hadn't even considered the need for. I mean, the game is\n\topen-source \u003ca href=\"/blog/2022-09-04\"\u003e📝 and I made it easy to build\u003c/a\u003e.\n\tYou can just fork the code, implement all the practice features you want in\n\ta much more efficient way, and I'd probably even merge your code into my\n\tbuilds then?\u003cbr\u003e\n\tBut I get it – recompiling the game yields just yet another build that can't\n\tbe easily compared to the original release. A cheat table is much more\n\ttrustworthy in giving players the confidence that they're still practicing\n\tthe same original game. And given the current priorities of my backers,\n\tit'll still take a while for me to implement proof by replay validation,\n\twhich will ultimately free every part of the community from depending on the\n\toriginal builds of both Seihou and PC-98 Touhou.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, such an implementation within the original binary layout would\n\tsignificantly drive up the budget of non-ASCII translations, and I sure\n\tdon't want to constantly maintain this layout during development. So, let's\n\tchase TH04 position independence like it's 2020, and quickly cover a larger\n\tamount of PI-relevant structures and functions at a shallow level. The only\n\tparts I decompiled for now contain calculations whose intent can't be\n\tclearly communicated in ASM. Hitbox visualizations or other more in-depth\n\tresearch would have to wait until I get to the proper decompilation of these\n\tfeatures.\u003cbr\u003e\n\tBut even this shallow work left us with a large amount of TH04-exclusive\n\tcode that had its worst parts RE'd and could be decompiled fairly quickly.\n\tIf you want to see big TH04 finalization% gains, general TH04 progress would\n\tbe a very good investment.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe first push went to the often-mentioned stage-specific custom entities\n\tthat share a single statically allocated buffer. Back in 2020, I\n\t\u003ca href=\"/blog/2020-02-29\"\u003e📝 wrongly claimed that these were a TH05 innovation\u003c/a\u003e,\n\tbut the system actually originated in TH04. Both games use a 26-byte\n\tstructure, but TH04 only allocates a 32-element array rather than TH05's\n\t64-element one. The conclusions from back then still apply, but I also kept\n\twondering why these games used a static array for these entities to begin\n\twith. You know what they call an area of memory that you can cleanly\n\trepurpose for things? That's right, a heap! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tAnd absolutely no one would mind one additional heap allocation at the start\n\tof a stage, next to the ones for all the sprites and portraits.\u003cbr\u003e\n\tHowever, we are still running in Real Mode with segmented memory. Accessing\n\tanything outside a common data segment involves modifying segment registers,\n\twhich has a nonzero CPU cycle cost, and Turbo C++ 4.0J is terrible at\n\toptimizing away the respective instructions. Does this matter? Probably not,\n\tbut you don't take \"risks\" like these if you're in a permanent\n\tmicro-optimization mindset… \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tIn TH04, this system is used for:\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cp\u003e\n\tKurumi's symmetric bullet spawn rays, fired from her hands towards the left\n\tand right edges of the playfield. These are rather infamous for being the\n\tlast thing you see before\n\t\u003ca href=\"/blog/2022-04-18\"\u003e📝 the \u003ccode\u003eDivide Error\u003c/code\u003e crash that can happen in ZUN's original build\u003c/a\u003e.\n\tCapped to 6 entities.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg class=\"inline_sprite\"\n\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAgBAMAAAAoDG0WAAAAHlBMVEUAAAD87O2Yl/zMVM39REUgqB8AZAB0AHSJAAAAAd0qD5i/AAAAAXRSTlMAQObYZgAAAk9JREFUeJyllT2u4zAMhJ0bhP45QNS5NFS4NqAirQul38LqH1z4Ai5Uu/NtH4bSALaABRYbFgEYZL5wJIqsKg0RkYrxP7lzzvEb5NZae82NMeaae+/9NXfvGHcSoB+3bSUB+v48DxKg/4SwkJD1JFBPAvUkUE9CVVUP56KGc0/Nrd00rE25MaeGMSn3Pmh4r3nVZn2MbkLeZP222QF5nfXnaV7Iu6wPwc+pgDcBWsIjGWAJ1SMZYAnVIxlgCQBQH/cEoH5bE4D680gA6sOigPYKmK4OthUe6ivgdXUQFvXQ5isgoMlXoBYAoANYAIAOYGHmEcQJHOfcMx/BIOO2Wmuf6QgOY4x+PNMRLJ/lExaPQ0iXuE/tHvcEwH+vQzNCDwAsvF51DT0AsPCnmzvo/bNqFeCm1mlMjQIGkQZ6O9QJIFJDb16dAuZu7qD3MwDvuONxEGBR/EBAbeDhBkDxnT6HDHDR3QDWroOIEGCMOWCBAMjw+44A59x+s4CXKAMrUBkEN4AXYQVZeAc0g4wEaEjdE6C6bvYlQC+BAMtbIEDq/iBAh4H45QqIUxuVkAHsAwIOkV77gIBZOu2DfIhRAZEANKGM6MQEQBNKj05MgBAW8R060c9opDcBe2oktOLaaD9rI2krvmrtRzTSJ4TZzzqUUicCsL/vgJ/xDjiP/grwYQkZ8JfXuP7ra/z2Od8n0l5MpLWYSEcxkZY8kb4cad8OVZZwHetjMdb7Yqx/7mOdi8UVi8UWi8UUi8UXq8kVq80Wq80Uq61Yjt8t12/W+y9a9u+HWMq6EgAAAABJRU5ErkJggg==\"\n\t\u003e The 4 \u003ca href=\"/blog/2022-04-18\"\u003e📝 bits\u003c/a\u003e used in Marisa's Stage 4 boss\n\tfight. Coincidentally also related to the rare \u003ccode\u003eDivide Error\u003c/code\u003e\n\tcrash in that fight.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg class=\"inline_sprite\"\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPIBAAAAAP//Vf/P3//v75qa/wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFDgABACwAAAAAIAAgAAADoBi6rPTwtUnZGzgPIau/WihwhNeAYTqSZkCkcLZyX2zP9PTaMZ5bvB6uBAyqfD+XDCdEEpUrgBQw00SlOACRMJp6qUjBNFviir9f5HgmfaDf4PBVS4Cj5ex23Z7Ge918Xn5/e4GCWWiAYxuGfIpSGY12ZYKMkokOl41PhZpvTy6ekyeimKSlfxSdoqCnrC2Phh0tLqufrbS1cLO5OhEQtAkAIfkEBQ4AAQAsAQABAB4AHAAAA4sYuksEMALhmL3ByS3EqBijbWQ3fESYkSzknR/2tOwLp2I9CK194orZBtaD/RpFns4YW9FovqPwWYr+plSJifnJ1qxELwd8EmvJZXNny01n1+t2+A1HD7z1uByfJ1PofVwUWF9rEWw7FE5QcId5OIRjhi6PSE99lZaMmGtAQVScHZ6aPZijIpFnnaMJACH5BAUOAAEALAMAAQAcAB4AAAOAGLpBBDBCx2p1MkdqF9bgRHRPaE7XqQKjUq5n+8JhS6/jreZ63EeCgUAy0w2OP1BwaBI4n0nAcwrVUa/WK5am3a6oQG9IGyYrBQQw5Hk8TiXsdLdNF1Lbo3l9vx/Jp3yBeC5XgoItDXeGfReAi3SIDA5sjxwdDXKGlpceaXybFQkAIfkEBQ4AAQAsAQADAB4AHAAAA4wYqkTyD8hJQVuYwR1rJZnGcd6XOeNYUuCCpts6tcEbS6RsiTnQryDUYDjo+G66hoBIhAl0EgeTCYNKptSUFYDNIqHd5k8XHn6t5eJzSykb2dfwG07oztkN+xoerWH3fC11U4AUS4UzLoQrRCU0flQrh44ni3QhkGaXmDWDdx4XnC51n1GPoqOIFqcYCQAh+QQFDgAEACwAAAAAIAAgAAADoEi6rPHwtUnZGzgPIau/WihwgdeAYTqSJhGkcLZyX2zP9PTaMZ5bvB6uBAyqfD+XDCdEEpUrgBQw00SlOAAxMJp6qUjBNFviir9f5HgmfaDf4PBVG4Cj5ex23Z7Ge918Xn5/e4GCWWiAYxuGfIpSGY12ZYKMkokOl41PhZpvTy6ekyeimKSlfxSdoqCnrC2Phh0tLqufrbS1cLO5OhEQtAkAIfkEBQ4ABAAsAQABAB4AHAAAA4tIuhsBMALhmL3EyS3EqBijbWQ3fEGYkSzknR/2tOwLp2I9CK194orZBtaD/RpFns4YW9FovqPwWYr+plSJifnJ1qxELwd8EmvJZXNny01n1+t2+A1HD7z1uByfJ1PofVwUWF9rEWw7FE5QcId5OIRjhi6PSE99lZaMmGtAQVScHZ6aPZijIpFnnaMJACH5BAUOAAQALAMAAQAcAB4AAAOASLoUATBCx2p1MkdqF9bgFHRPaE7XqQKjUq5n+8JhS6/jreZ63EeCgUAy0w2OP1BwaBI4n0nAcwrVUa/WK5am3a6oQG9IGyYrBQEw5Hk8TiXsdLdNF1Lbo3l9vx/Jp3yBeC5XgoItDXeGfReAi3SIDA5sjxwdDXKGlpceaXybFQkAIfkEBQ4ABAAsAQADAB4AHAAAA4xIqhHyD8hJQVuYwR1rDZnGcd6XOeNYUuCCpts6tcQbS6RsiTnQryDUYDjo+G66hoBIhAl0EgeTCYNKptSUFYDNIqHd5k8XHn6t5eJzSykb2dfwGx7oztkN+xoerWH3fC11U4AUS4UzLoQrRCU0flQrh44ni3QhkGaXmDWDdx4XnC51n1GPoqOIFqcYCQA7\n\t\"\u003e Stage 4 Reimu's spinning orbs. Note how the game uses two different sets\n\tof sprites just to have two different outline colors. This was probably\n\tbetter than messing with the palette, which can easily cause unintended\n\teffects if you only have 16 colors to work with. Heck, \u003ca\n\thref=\"/blog/tag/palette\"\u003eI have an entire blog post tag just to highlight\n\tthese cases\u003c/a\u003e. Capped to the full 32 entities.\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\t\u003cimg class=\"inline_sprite\"\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAP9FRf+quv///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDgADACwAAAAAIAAgAAACwJyPCAvp75hcsCoZcqAW4iyEW9NFlBaKXKkYWDqyDzMsgUoe5DrrwC2I1RqcXIWxuSWJTJcxQRsqf8nRxlWy/YI/ajf2hHqBVm1XcsxQcUlu9TqcaKzuIMzOjU/2ZGDqz4U0NwgCaAjzcnhISJgoAkIXuLdX0zf4JfClNumioaVG9xkmtiS35alm8bXTuRWYmgZ74QcaUBPLI8Vmmys24xpUNQr122cjK+NEJppM+ujV7GwFF+0DGlXto5Xtgd1RAAAh+QQJDgADACwBAAAAHwAfAAACxpyPAwvamqI8rYLA1tzyBqxxk3UJ5heKFIMCpgCqo/edqWywS2Df8g7i9TiZxwH1Gm6KoV1JmTM6PBlU0BZwTB+7U+vyhDk/WRRF+Pp6xBgrOetAJ8nPoK5t4ZFh6H4b/5en5pL0AiRAlQh295Z28hgYx6WAV9I416KlUQHIU1WTtlgRNUDnCQSEdpGjSWnFV8mit2nE1ebFEKXjk9BFF8FEtEYnhcDba2mG09ElVrwcJWQ1Ch0dmlktqxf4rPJJE1O9RSJTAAAh+QQJDgADACwBAAAAHgAfAAACwZyPaQDq/1iDlLJa75RjOv4FEhCIl8KYiEamgmB6C1meXSvmL2wvJawy1CQ7YM/1GuJoxeEKmczFAs3gIUWt0kRa2Y3mog6z0d5ViTWSnagGC1n61USdhGYhJO/m8Xud1SGll7IVULdyM6A0CJajaPh0M1Yk0FJYE2KSpVM2Queg+WK5hlMSAbfYhOX1tTkFJegl4YrpuQdyhdryRJJkNrunxJv1yyRqlgvGmsb2McIq2RfjjLySA2aBEf2pnV0NUQAAIfkEBQ4AAwAsAQABAB8AHwAAAsicf6CA7Y+WgobRqEKYb92xaMJmYVXZKcJKlgwHQpm4jtwEtCB6arRtyQgljUkrUNtockylz/UK5pDK6REJPO0qStuTSmV5EjsJVVhLj0gO3EYMWKufvG0zmZu3xsVld5S0RERhlZEmOHhB9yMwE/BxEsLoN1UX6Vhj1di0EYOBidf1RZcwE+hDEvblGYnFQne34shnuIYohOVUiSFSBWMqptSx2pYH5JMSUle7xkRIWmQsdGElk6cDuVVcaUlonZGdLdEd3nZRAAA7\"\n\t\u003e The chasing cross bullets, seen in Phase 14 of the same Stage 6 Yuuka\n\tfight. Featuring some smart sprite work, making use of point symmetry to\n\tachieve a fluid animation in just 4 frames. This is\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e in sprite form. Capped to 31 entities, because the 32\u003csup\u003end\u003c/sup\u003e custom entity during this fight is defined to be…\n\u003c/p\u003e\u003c/li\u003e\u003cli\u003e\u003cp\u003e\n\tThe single purple pulsating and shrinking safety circle, seen in Phase 4 of\n\tthe same fight. The most interesting aspect here is actually still related\n\tto the cross bullets, whose spawn function is wrongly limited to 32 entities\n\tand could theoretically overwrite this circle. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This\n\tis strictly landmine territory though:\u003c/p\u003e\u003cul\u003e\n\t\t\u003cli\u003eYuuka never uses these bullets and the safety circle\n\t\tsimultaneously\u003c/li\u003e\n\t\t\u003cli\u003eShe never spawns more than 24 cross bullets\u003c/li\u003e\n\t\t\u003cli\u003eAll cross bullets are fast enough to have left the screen by the\n\t\ttime Yuuka restarts the corresponding subpattern\u003c/li\u003e\n\t\t\u003cli\u003eThe cross bullets spawn at Yuuka's center position, and assign its\n\t\tQ12.4 coordinates to structure fields that the safety circle interprets\n\t\tas raw pixels. The game does try to render the circle afterward, but\n\t\tsince Yuuka's static position during this phase is nowhere near a valid\n\t\tpixel coordinate, it is immediately clipped.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe flashing lines seen in Phase 5 of the Gengetsu fight,\n\ttelegraphing the slightly random bullet columns.\u003c/p\u003e\n\t\u003cfigure class=\"singleplayer_playfield\"\u003e\n\t\t\u003crec98-child-switcher\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-05-29-TH04-Gengetsu-spawn-columns-0.png?3dee2b77\"\n\t\t\tdata-title=\"First color\"\n\t\t\talt=\"The spawn column lines in the TH05 Gengetsu fight, in the first of their two flashing colors.\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-05-29-TH04-Gengetsu-spawn-columns-1.png?7c063d18\"\n\t\t\tdata-title=\"Second color\"\n\t\t\talt=\"The spawn column lines in the TH05 Gengetsu fight, in the second of their two flashing colors.\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThese structures only took 1 push to reverse-engineer rather than the 2 I\n\tneeded for their TH05 counterparts because they are much simpler in this\n\tgame. The \"structure\" for Gengetsu's lines literally uses just a single X\n\tposition, with the remaining 24 bytes being basically padding. The only\n\tminor bug I found on this shallow level concerns Marisa's bits, which are\n\tclipped at the right and bottom edges of the playfield 16 pixels earlier\n\tthan you would expect:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.webp?b0de2e27\" preload=\"none\" controls data-title=\"Original clipping\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"352\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.avi?2489ab1f\"\u003e\u003csource src=\"/blog/static/video/av1/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.webm?34102207\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.webm?1418f1cb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.webm?98113c7d\" type=\"video/webm\"\u003eVideo of the bit spin-out animation in TH04's Stage 4 Marisa fight, demonstrating the slightly wrong clipping conditions from the original game. \u003ca href=\"/blog/static/video/zmbv/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-original.avi?2489ab1f\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"159\" data-title=\"Bottom edge\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"220\" data-title=\"Right edge\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.webp?b0de2e27\" preload=\"none\" controls data-title=\"Fixed clipping\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"352\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.avi?01252764\"\u003e\u003csource src=\"/blog/static/video/av1/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.webm?c56ecb09\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.webm?017032d6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.webm?e9d6df86\" type=\"video/webm\"\u003eVideo of the bit spin-out animation in TH04's Stage 4 Marisa fight, with fixed clipping conditions. \u003ca href=\"/blog/static/video/zmbv/2023-05-29-TH04-Stage-4-Marisa-bit-clipping-fixed.avi?01252764\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"159\" data-title=\"Bottom edge\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"220\" data-title=\"Right edge\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tThe remaining push went to a bunch of smaller structures and functions:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\n\tThe structure for the up to 2 \"thick\" (a.k.a. \"Master Spark\") lasers. Much\n\tsaner than the\n\t\u003ca href=\"/blog/2023-01-17\"\u003e📝 madness of TH05's laser system\u003c/a\u003e while being\n\tequally customizable in width and duration.\n\u003c/li\u003e\u003cli\u003e\n\tThe structure for the various monochrome 16×16 shapes in the background of\n\tthe Stage 6 Yuuka fight, drawn on top of the checkerboard. \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAA3f///yH5BAUKAAEALAAAAAAQABAAAAIjjI+pBrDa2kPRzVAlzbzizh2gt4HQeJolKpJZG62W9tAXUgAAOw==\" alt=\"\"\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAA3f///yH5BAUKAAEALAAAAAAQABAAAAIrjA2Aes2PWlA00XkYzjxK52nSY31O4plbapLiZ0FtqKaaeHMVeoGbDCNJCgA7\" alt=\"\"\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAA3f///yH5BAUKAAEALAAAAAAQABAAAAIojB8AyKzdlFwwmWfjldft2R3LBFLkiaZnl2bh6r5cLG4hBFe2jmBMAQA7\" alt=\"\"\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAA3f///yH5BAUKAAEALAAAAAAQABAAAAIpjA2Zx6gPWoCUvXZdlEqf7oFQuExZJ54LaG6tx5Wtq9HUiN10zkq+VAAAOw==\" alt=\"\"\u003e\n\u003c/li\u003e\u003cli\u003e\n\tThe rendering code for the three falling stars in the background of Stage 5.\n\tThe effect here is entirely palette-related: After blitting the stage tiles,\n\tthe \u003ca href=\"/blog/2021-11-29\"\u003e📝 1bpp star image\u003c/a\u003e is \u003ccode\u003eOR\u003c/code\u003eed\n\tinto only the 4\u003csup\u003eth\u003c/sup\u003e VRAM plane, which is equivalent to setting the\n\thighest bit in the palette color index of every pixel within the star-shaped\n\tregion. This of course raises the question of how the stage would look like\n\tif it was fully illuminated: \u003cfigure class=\"s5-2023-05-29\"\u003e\n\t\t\u003crec98-child-switcher style=\"width: 192px;\"\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-05-29-TH04-Stage-5-regular.png?34dc26c3\"\n\t\t\tdata-title=\"Lights off\"\n\t\t\talt=\"The full tile map of TH04's Stage 5, without scrolling stars illuminating parts of the map.\"\n\t\t\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2023-05-29-TH04-Stage-5-bright.png?0b04b350\"\n\t\t\tdata-title=\"Lights on\"\n\t\t\talt=\"The full tile map of TH04's Stage 5, with the illumination effect of the scrolling star animation applied equally to the entire map.\"\n\t\t\tclass=\"active\"\n\t\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tThe full tile map of TH04's Stage 5, in both dark and fully\n\t\t\tilluminated views. Since the illumination effect depends on two\n\t\t\tmatching sets of palette colors that are distinguished by a single\n\t\t\tbit, the illuminated view is limited to only 8 of the 16 colors. The\n\t\t\tdark view, on the other hand, can freely use colors from the\n\t\t\tilluminated set, since those are unaffected by the \u003ccode\u003eOR\u003c/code\u003e\n\t\t\toperation.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/li\u003e\u003cli\u003e\n\tMost code that modifies a stage's tile map, and directly specifies tiles via\n\ttheir top-left offset in VRAM.\u003cbr\u003e\n\tThanks to code alignment reasons, this forced a much longer detour into the\n\t.STD format loader. Nothing all too noteworthy there since we're still\n\tmissing the enemy script and spawn structures before we can call .STD\n\t\"reverse-engineered\", but maybe still helpful if you're looking for an\n\toverview of the format. Also features a buffer overflow landmine if a .STD\n\tfile happens to contain more than 32 enemy scripts… you know, the usual\n\tstuff.\n\u003c/li\u003e\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tTo top off the second push, we've got the vertically scrolling checkerboard\n\tbackground during the Stage 6 Yuuka fight, made up of 32×32 squares. This\n\tone deserves a special highlight just because of its needless complexity.\n\tYou'd think that even a performant implementation would be pretty simple:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eSet the GRCG to TDW mode\u003c/li\u003e\n\t\u003cli\u003eSet the GRCG tile to one of the two square colors\u003c/li\u003e\n\t\u003cli\u003eStart with \u003cvar\u003eY\u003c/var\u003e as the current scroll offset, and \u003cvar\u003eX\u003c/var\u003e\n\tas some indicator of which color is currently shown at the start of each row\n\tof squares\u003c/li\u003e\n\t\u003cli\u003eIterate over all lines of the playfield, filling in all pixels that\n\tshould be displayed in the current color, skipping over the other ones\u003c/li\u003e\n\t\u003cli\u003eCount down \u003cvar\u003eY\u003c/var\u003e for each line drawn\u003c/li\u003e\n\t\u003cli\u003eIf \u003cvar\u003eY\u003c/var\u003e reaches 0, reset it to 32 and flip \u003cvar\u003eX\u003c/var\u003e\u003c/li\u003e\n\t\u003cli\u003eAt the bottom of the playfield, change the GRCG tile to the other color,\n\tand repeat with the initial value of \u003cvar\u003eX\u003c/var\u003e flipped\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThe most important aspect of this algorithm is how it reduces GRCG state\n\tchanges to a minimum, avoiding the costly port I/O that we've identified\n\ttime and time again as one of the main bottlenecks in TH01. With just 2\n\tstate variables and 3 loops, the resulting code isn't that complex either. A\n\tnaive implementation that just drew the squares from top to bottom in a\n\tsingle pass would barely be simpler, but much slower: By changing the GRCG\n\ttile on every color, such an implementation would burn a low 5-digit number\n\tof CPU cycles per frame for the 12×11.5-square checkerboard used in the\n\tgame.\u003cbr\u003e\n\tAnd indeed, ZUN retained all important aspects of this algorithm… but still\n\timplemented it all in ASM, with a ridiculous layer of x86 segment arithmetic\n\ton top? \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Which blows up the complexity to 4 state\n\tvariables, 5 nested loops, and a bunch of constants in unusual units. I'm\n\tnot sure what this code is supposed to optimize for, especially with that\n\trather questionable register allocation that nevertheless leaves one of the\n\tgeneral-purpose registers unused. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Fortunately,\n\tthe function was still decompilable without too many code generation hacks,\n\tand retains the 5 nested loops in all their \u003ccode\u003egoto\u003c/code\u003e-connected\n\tglory. If you want to add a checkerboard to your next \u003ca\n\thref=\"https://lainnet.superglobalmegacorp.com/blog/2023_05_11_n02.html\"\u003ePC-98\n\tdemo\u003c/a\u003e, just stick to the algorithm I gave above.\u003cbr\u003e\n\t(Using a single XOR for flipping the starting X offset between 32 and 64\n\tpixels \u003ci\u003eis\u003c/i\u003e pretty nice though, I have to give him that.)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThis makes for a good occasion to talk about the third and final GRCG mode,\n\tcompleting the series I started with my previous coverage of the\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 RMW\u003c/a\u003e and\n\t\u003ca href=\"/blog/2022-01-31\"\u003e📝 TCR\u003c/a\u003e modes. The TDW (Tile Data Write) mode\n\tis the simplest of the three and just writes the 8×1 GRCG tile into VRAM\n\tas-is, without applying any alpha bitmask. This makes it perfect for\n\tclearing rectangular areas of pixels – or even all of VRAM by doing a single\n\t\u003ccode\u003ememset()\u003c/code\u003e:\n\u003c/p\u003e\u003cpre\u003e// Set up the GRCG in TDW mode.\noutportb(0x7C, 0x80);\n\n// Fill the tile register with color #7 (0111 in binary).\noutportb(0x7E, 0xFF); // Plane 0: (B): (********)\noutportb(0x7E, 0xFF); // Plane 1: (R): (********)\noutportb(0x7E, 0xFF); // Plane 2: (G): (********)\noutportb(0x7E, 0x00); // Plane 3: (E): (        )\n\n// Set the 32 pixels at the top-left corner of VRAM to the exact contents of\n// the tile register, effectively repeating the tile 4 times. In TDW mode, the\n// GRCG ignores the CPU-supplied operand, so we might as well just pass the\n// contents of a register with the intended width. This eliminates useless load\n// instructions in the compiled assembly, and even sort of signals to readers\n// of this code that we do not care about the source value.\n*reinterpret_cast\u0026lt;uint32_t far *\u0026gt;(MK_FP(0xA800, 0)) = _EAX;\n\n// Fill the entirety of VRAM with the GRCG tile. A simple C one-liner that will\n// probably compile into a single `REP STOS` instruction. Unfortunately, Turbo\n// C++ 4.0J only ever generates the 16-bit `REP STOSW` here, even when using\n// the `__memset__` intrinsic and when compiling in 386 mode. When targeting\n// that CPU and above, you'd ideally want `REP STOSD` for twice the speed.\nmemset(MK_FP(0xA800, 0), _AL, ((640 / 8) * 400));\n\u003c/pre\u003e\u003cp\u003e\n\tHowever, this might make you wonder why TDW mode is even necessary. If it's\n\tfunctionally equivalent to RMW mode with a CPU-supplied bitmask made up\n\tentirely of 1 bits (i.e., \u003ccode\u003e0xFF\u003c/code\u003e, \u003ccode\u003e0xFFFF\u003c/code\u003e, or\n\t\u003ccode\u003e0xFFFFFFFF\u003c/code\u003e), what's the point? The difference lies in the\n\thardware implementation: If all you need to do is \u003ci\u003ewrite\u003c/i\u003e tile data to\n\tVRAM, you don't need the \u003ci\u003eread\u003c/i\u003e and \u003ci\u003emodify\u003c/i\u003e parts of RMW mode\n\twhich require additional processing time. The \u003ci\u003ePC-9801 Programmers'\n\tBible\u003c/i\u003e claims a speedup of almost 2× when using TDW mode over equivalent\n\toperations in RMW mode.\u003cbr\u003e\n\tAnd that's the only performance claim I found, because none of these old\n\tPC-98 hardware and programming books did any benchmarks. Then again, it's\n\tnot too interesting of a question to benchmark either, as the byte-aligned\n\tnature of TDW blitting severely limits its use in a game engine anyway.\n\tSure, \u003ci\u003emaybe\u003c/i\u003e it makes sense to temporarily switch from RMW to TDW mode\n\tif you've identified a large rectangular and byte-aligned section within a\n\tsprite that could be blitted without a bitmask? But the necessary\n\tidentification work likely nullifies the performance gained from TDW mode,\n\tI'd say. In any case, that's pretty deep\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/micro-optimization\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/a\u003e\u003c/span\u003e territory. Just use TDW mode for the\n\tfew cases it's good at, and stick to RMW mode for the rest.\n\u003c/p\u003e\u003cp\u003e\n\tSo is this all that can be said about the GRCG? Not quite, because there are\n\t4 bits I haven't talked about yet…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd now we're just 5.37% away from 100% position independence for TH04! From\n\tthis point, another 2 pushes should be enough to reach this goal. It might\n\tnot look like we're \u003ci\u003ethat\u003c/i\u003e close based on the current estimate, but a\n\tbig chunk of the remaining numbers are false positives from the player shot\n\tcontrol functions. Since we've got a very special deadline to hit, I'm going\n\tto cobble these two pushes together from the two current general\n\tsubscriptions and the rest of the backlog. But you can, of course, still\n\tinvest in this goal to allow the existing contributions to go to something\n\telse.\u003cbr\u003e\n\t… Well, if the store was actually open. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e So I'd better\n\tcontinue with a quick task to free up some capacity sooner rather than\n\tlater. Next up, therefore: Back to TH02, and its item and player systems.\n\tShouldn't take that long, I'm not expecting any surprises there. (Yeah, I\n\tknow, famous last words…)\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-06-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-05-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-05-29T23:39:52Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-05-01",
      "url": "https://rec98.nmlgc.net/blog/2023-05-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-05-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-05-01\"\u003e\u003ctime datetime=\"2023-05-01T23:56:59Z\"\u003e2023-05-01 23:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0238\"\u003eP0238\u003c/a\u003e\n\t\t\tWebsite (TypeScript migration + Stripe integration, part 1/2: Basic support)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/4698397...edf2926\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0239\"\u003eP0239\u003c/a\u003e\n\t\t\tWebsite (Stripe integration, part 2/2: Subscription support) + TH01 Anniversary Edition (Minor bugfixes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c5e51e6...P0239\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/anniversary-edition\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Polished and bugfixed forks of 100% decompiled games.\"\u003eanniversary-edition\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debloating\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Cleaning up ZUN\u0026#39;s original code into something you can actually read and maintain.\"\u003edebloating\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Debug features that ZUN left in the shipped game.\"\u003edebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\u003cp\u003e\n\t\u003cimg src=\"/static/emoji-stripe.svg?2fd927b6\" alt=\":stripe:\" width=\"24\" height=\"24\" \u003e \u003ca href=\"https://stripe.com\"\u003eStripe\u003c/a\u003e is now\n\tproperly integrated into this website as an alternative to PayPal! Now, you\n\tcan also financially support the project if PayPal doesn't work for you, or\n\tif you prefer \u003ca\n\thref=\"https://stripe.com/docs/payments/payment-methods/overview\"\u003eusing a\n\tprovider out of Stripe's greater variety\u003c/a\u003e. It's unfortunate that I had to\n\tship this integration while the store is still sold out, but the Shuusou\n\tGyoku OpenGL backend has turned out way too complicated to be finished next\n\tto these two pushes within a month. It will take quite a while until the\n\tstore reopens and you all can start using Stripe, so I'll just link back to\n\tthis blog post when it happens.\n\u003c/p\u003e\u003cp\u003e\n\tIntegrating Stripe wasn't the simplest task in the world either. At first,\n\tthe \u003ca href=\"https://stripe.com/docs/payments/checkout\"\u003eCheckout API\u003c/a\u003e\n\tseems pretty friendly to developers: The entire payment flow is handled on\n\tthe backend, in the server language of your choice, and requires no frontend\n\tJavaScript except for the UI feedback code you \u003ci\u003echoose\u003c/i\u003e to write. Your\n\tbackend API endpoint initiates the Stripe Checkout session, answers with a\n\tredirect to Stripe, and Stripe then sends a redirect back to your server if\n\tthe customer completed the payment. Superficially, this server-based\n\tapproach seems much more GDPR-friendly than PayPal, because there are no\n\tremote scripts to obtain consent for. In reality though, Stripe shares\n\t\u003ci\u003emuch\u003c/i\u003e more potential personal data about your credit card or bank\n\taccount with a merchant, compared to PayPal's almost bare minimum of\n\tnecessary data. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIt's also rather annoying how the backend has to persist the order form\n\tinformation throughout the entire Checkout session, because it would\n\totherwise be lost if the server restarts while a customer is still busy\n\tentering data into Stripe's Checkout form. Compare that to the PayPal\n\tJavaScript SDK, which only \u003ccode\u003ePOST\u003c/code\u003es back to your server after the\n\tcustomer completed a payment. In Stripe's case, more JavaScript actually\n\tonly makes the integration \u003ci\u003eharder\u003c/i\u003e: If you trigger the initial payment\n\tHTTP request from JavaScript, you will \u003ca\n\thref=\"https://github.com/whatwg/fetch/issues/763#issuecomment-1430650132\"\u003ehave\n\tto improvise a bit to avoid the CORS error when redirecting away to a\n\tdifferent domain\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tBut sure, it's all not too bad… for regular orders at least. With\n\tsubscriptions, however, things get \u003ci\u003emuch\u003c/i\u003e worse. Unlike PayPal, Stripe\n\tkind of wants to stay out of the way of the payment process as much as\n\tpossible, and just be a wrapper around its supported payment methods. So if\n\tcustomers aren't really meant to register with Stripe, how would they cancel\n\ttheir subscriptions? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAnswer: \u003ca\n\thref=\"https://support.stripe.com/questions/cancelling-a-subscription-made-through-stripe\"\u003eThrough\n\tthe… merchant?\u003c/a\u003e Which I quite dislike in principle, because why should\n\tyou have to trust me to actually cancel your subscription after you\n\trequested it? It also means that I probably should add some sort of UI for\n\tself-canceling a Stripe subscription, ideally without adding full-blown user\n\taccounts. Not that this solves the underlying trust issue, but it's more\n\tconvenient than contacting me via email or, worse, going through your bank\n\tsomehow. Here is how my solution works:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhen setting up a Stripe subscription, the server will generate a random\n\tID for authentication. This ID is then used as a \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Salt_(cryptography)\"\u003esalt\u003c/a\u003e for a hash\n\tof the Stripe subscription ID, linking the two without storing the latter on\n\tmy server.\u003c/li\u003e\n\t\u003cli\u003eThe \u003cq\u003ethank you\u003c/q\u003e page, which is parameterized with the Stripe\n\t\u003ci\u003eCheckout session\u003c/i\u003e ID, will use that ID to retrieve the \u003ci\u003esubscription\n\tID\u003c/i\u003e via an API call to Stripe, and display it together with the above\n\tsalt. This works indefinitely – contrary to what the expiry field in the\n\t\u003ci\u003eCheckout session\u003c/i\u003e object suggests, Stripe sessions are indeed \u003ca\n\thref=\"https://stackoverflow.com/questions/69600941/are-stripes-checkout-sessions-stored-forever\"\u003estored\n\tforever\u003c/a\u003e. After all, Stripe also displays this session information in a\n\tmerchant's transaction log with an excessive amount of detail. It might have\n\tbeen better to add my own expiration system to these pages, but this had\n\tbeen taking long enough already. For now, be aware that sharing the link to\n\ta Stripe \u003cq\u003ethank you\u003c/q\u003e page is equivalent to sharing your subscription\n\tcancellation password.\u003c/li\u003e\n\t\u003cli\u003eThe salt is then used as the key for a subscription management page. To\n\tcancel, you visit this page and enter the Stripe subscription ID to confirm.\n\tThe server then checks whether the salt and subscription ID pair belong to\n\teach other, and sends the actual \u003ca\n\thref=\"https://stripe.com/docs/api/subscriptions/cancel\"\u003ecancellation\n\trequest\u003c/a\u003e back to Stripe if they do.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tI might have gone a bit overboard with the crypto there, but I liked the\n\tidea of not storing any of the Stripe session IDs in the server database.\n\tIt's not like that makes the system more complex anyway, and it's nice to\n\thave a separate confirmation step before canceling a subscription.\n\u003c/p\u003e\u003cp\u003e\n\tBut even \u003ci\u003ethat\u003c/i\u003e wasn't everything I had to keep in mind here. Once you\n\tswitch from test to production mode for the final tests, you'll notice that\n\tcertain \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Single_Euro_Payments_Area\"\u003eSEPA\u003c/a\u003e-based\n\tpayment providers take their sweet time to process and activate new\n\tsubscriptions. The Checkout session object even informs you about that, by\n\tincluding a \u003cq\u003epayment status\u003c/q\u003e field. Which initially seems just like\n\tanother field that could indicate hacking attempts, but treating it as such\n\tand rejecting any unpaid session can also reject perfectly valid\n\tsubscriptions. I don't \u003ci\u003ewant\u003c/i\u003e all this control… 🥲\u003cbr\u003e\n\tInstead, all I can do in this case is to tell you about it. In my test, the\n\tStripe dashboard said that it might take days or even weeks for the initial\n\tsubscription transaction to be confirmed. In such a case, the respective\n\tfraction of the cap will unfortunately need to remain \u003cspan\n\tclass=\"incoming\"\u003ered\u003c/span\u003e for that entire time.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that was 1½ pushes just to replicate the basic functionality of a simple\n\tPayPal integration with the simplest type of Stripe integration. On the\n\tarchitectural site, all the necessary refactoring work made me finally\n\tupgrade my frontend code to TypeScript at least, using the amazing \u003ca\n\thref=\"https://esbuild.github.io/\"\u003eesbuild\u003c/a\u003e to handle transpilation inside\n\tthe server binary. Let's see how long it will now take for me to upgrade to\n\tSCSS…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith the new payment options, it makes sense to go for another slight price\n\tincrease, from \u003cscript\u003eformatCurrency(7500)\u003c/script\u003e\u003cnoscript\u003e75.00\u0026nbsp;€\u003c/noscript\u003e up to \u003cscript\u003eformatCurrency(8500)\u003c/script\u003e\u003cnoscript\u003e85.00\u0026nbsp;€\u003c/noscript\u003e per push.\n\tThe amount of taxes I have to pay on this income is slowly becoming\n\tsignificant, and the store has been selling out almost immediately for the\n\tlast few months anyway. If demand remains at the current level or even\n\tincreases, I plan to gradually go up to \u003cscript\u003eformatCurrency(12500)\u003c/script\u003e\u003cnoscript\u003e125.00\u0026nbsp;€\u003c/noscript\u003e by the end\n\tof the year. \u003cbr\u003e\n\t\u003ca href=\"/blog/2021-12-01\"\u003e📝 As\u003c/a\u003e \u003ca href=\"/blog/2022-08-15\"\u003e📝 usual\u003c/a\u003e,\n\tI'm going to deliver existing orders in the backlog at the value they were\n\toriginally purchased at. Due to the way the cap has to be calculated, these\n\tcontributions now appear to have increased in value by a rather awkward\n\t13.33%.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThis left ½ of a push for some more work on the TH01 Anniversary Edition.\n\tUnfortunately, this was too little time for the grand issue of removing\n\tbyte-aligned rendering of bigger sprites, which will need some additional\n\tblitting performance research. Instead, I went for a bunch of smaller\n\tbugfixes:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\t\u003cp\u003e\n\t\t\t\u003ccode\u003eANNIV.EXE\u003c/code\u003e now launches \u003ccode\u003eZUNSOFT.COM\u003c/code\u003e if\n\t\t\tMDRV98 wasn't resident before. In hindsight, it's completely obvious\n\t\t\twhy this is the right thing to do: Either you start\n\t\t\t\u003ccode\u003eANNIV.EXE\u003c/code\u003e directly, in which case there's no resident\n\t\t\tMDRV98 and you haven't seen the ZUN Soft logo, \u003ci\u003eor\u003c/i\u003e you have\n\t\t\tmade a single-line edit to \u003ccode\u003eGAME.BAT\u003c/code\u003e and replaced\n\t\t\t\u003ccode\u003eop\u003c/code\u003e with \u003ccode\u003eanniv\u003c/code\u003e, in which case MDRV98 is\n\t\t\tresident and you \u003ci\u003ehave\u003c/i\u003e seen the logo. These are the two\n\t\t\treasonable cases to support out of the box. If you are doing\n\t\t\tanything else, it shouldn't be \u003ci\u003ethat\u003c/i\u003e hard to adjust though?\n\t\t\u003c/p\u003e\u003cp\u003e\n\t\t\tYou might be wondering why I didn't just include all code of\n\t\t\t\u003ccode\u003eZUNSOFT.COM\u003c/code\u003e inside \u003ccode\u003eANNIV.EXE\u003c/code\u003e together with\n\t\t\tthe rest of the game. The reason: \u003ccode\u003eZUNSOFT.COM\u003c/code\u003e has\n\t\t\talmost nothing in common with regular TH01 code. While the rest of\n\t\t\tTH01 uses the custom image formats and bad rendering code I\n\t\t\tdocumented again and again during its RE process,\n\t\t\t\u003ccode\u003eZUNSOFT.COM\u003c/code\u003e fully relies on master.lib for everything\n\t\t\tabout the bouncing-ball logo animation. Its code is much closer to\n\t\t\tTH02 in that respect, which suggests that ZUN did in fact write this\n\t\t\tanimation \u003ci\u003efor\u003c/i\u003e TH02, and just included the binary in TH01 for\n\t\t\tconsistency when he first sold both games together at Comiket 52.\n\t\t\tUnlike the \u003ca href=\"/blog/2023-03-05#single-2023-03-05\"\u003e📝 various bad reasons for splitting the PC-98 Touhou games into three main executables\u003c/a\u003e,\n\t\t\tit's still a good idea to split off animations that use a completely\n\t\t\tdifferent set of rendering and file format functions. Combined with\n\t\t\tall the BFNT and shape rendering code, \u003ccode\u003eZUNSOFT.COM\u003c/code\u003e\n\t\t\tactually contains even more unique code than \u003ccode\u003eOP.EXE\u003c/code\u003e,\n\t\t\tand only slightly less than \u003ccode\u003eFUUIN.EXE\u003c/code\u003e.\n\t\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\tThe optional \u003ccode\u003eAUTOEXEC.BAT\u003c/code\u003e is now correctly encoded in\n\t\tShift-JIS instead of accidentally being UTF-8, fixing the previous\n\t\tmojibake in its final \u003ccode\u003eECHO\u003c/code\u003e line.\n\t\u003c/p\u003e\u003cli\u003e\n\t\t\u003cp\u003e\n\t\t\tThe command-line option that just adds a stage selection without\n\t\t\tother debug features (\u003ccode\u003eanniv s\u003c/code\u003e) now works reliably.\n\t\t\u003c/p\u003e\u003cp\u003e\n\t\t\tThis one's quite interesting because it only ever worked\n\t\t\t\u003ci\u003ebecause\u003c/i\u003e of a ZUN bug. From a superficial look at the code, it\n\t\t\tshouldn't: While the presence of an \u003ccode\u003e's'\u003c/code\u003e branch proves\n\t\t\tthat ZUN had such a mode during development, he nevertheless forgot\n\t\t\tto initialize the debug flag inside the resident structure within\n\t\t\tthis branch. This mode only ever worked because master.lib's\n\t\t\t\u003ccode\u003eresdata_create()\u003c/code\u003e function doesn't clear the resident\n\t\t\tstructure after allocation. If anything on the system previously\n\t\t\thappened to write something other than \u003ccode\u003e0x00\u003c/code\u003e,\n\t\t\t\u003ccode\u003e0x01\u003c/code\u003e, or \u003ccode\u003e0x03\u003c/code\u003e to the specific byte that\n\t\t\tthen gets repurposed as the debug mode flag, this lack of\n\t\t\tinitialization does in fact result in a distinct non-test and\n\t\t\tnon-debug stage selection mode. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\t\t\tThis is what happens on a certain widely circulated .HDI copy of\n\t\t\tTH01 that boots MS-DOS 3.30C. On this system, the memory that\n\t\t\tmaster.lib will allocate to the TH01 resident structure was\n\t\t\tpreviously used by DOS as stack for its kernel, which left the\n\t\t\tfuture resident debug flag byte at address \u003ccode\u003e9FF6:0012\u003c/code\u003e at\n\t\t\ta value of \u003ccode\u003e0x12\u003c/code\u003e. This might be the entire reason why\n\t\t\t\u003ccode\u003egame s\u003c/code\u003e is even widely documented to trigger a stage\n\t\t\tselection to begin with – on the widely circulated TH04 .HDI that\n\t\t\tboots MS-DOS 6.20, or on DOSBox-X, the \u003ccode\u003es\u003c/code\u003e parameter\n\t\t\tdoesn't work because both DOS systems leave the resident debug flag\n\t\t\tbyte at \u003ccode\u003e0x00\u003c/code\u003e. And since \u003ccode\u003eANNIV.EXE\u003c/code\u003e pushes\n\t\t\tMDRV98 into that area of conventional DOS RAM, \u003ccode\u003eanniv s\u003c/code\u003e\n\t\t\tpreviously didn't work even on MS-DOS 3.30C.\n\t\t\u003c/p\u003e\n\t\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\tBoth bugs in the\n\t\t\u003ca href=\"/blog/2021-10-09\"\u003e📝 1×1 particle system during the Mima fight\u003c/a\u003e\n\t\thave been fixed. These include the off-by-one error that killed off the\n\t\tvery first particle on the 80\u003csup\u003eth\u003c/sup\u003e\n\t\tframe and left it in VRAM, and, just like every other entity type, a\n\t\treplacement of ZUN's EGC unblitter with the new pixel-perfect and fast\n\t\tone. Until I've rearchitected unblitting as a whole, the particles will\n\t\tnow merely rip barely visible 1×1 holes into the sprites they overlap.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\tThe \u003ca href=\"/blog/2021-09-28\"\u003e📝 score popups for flipped cards\u003c/a\u003e are now displayed without the two frames of flicker.\n\t\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003e\n\t\t\u003cp\u003e\n\t\t\tThe \u003ccode\u003ebomb\u003c/code\u003e value shown in the lowest line of the in-game\n\t\t\tdebug mode output is now right-aligned together with the rest of the\n\t\t\tvalues. This ensures that the game always writes a consistent number\n\t\t\tof characters to TRAM, regardless of the magnitude of the\n\t\t\t\u003ccode\u003ebomb\u003c/code\u003e value, preventing the seemingly wrong\n\t\t\t\u003ccode\u003etimer\u003c/code\u003e values that appeared in the original game\n\t\t\twhenever the value of the \u003ccode\u003ebomb\u003c/code\u003e variable changed to a\n\t\t\tlower number of digits:\n\t\t\u003c/p\u003e\u003cfigure\u003e\n\t\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-05-01-TH01-Debug-bomb-alignment-Original.webp?d38702d1\" preload=\"none\" controls data-title=\"Original game\" loop data-active width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"105\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2023-05-01-TH01-Debug-bomb-alignment-Original.avi?eea24ad1\"\u003e\u003csource src=\"/blog/static/video/av1/2023-05-01-TH01-Debug-bomb-alignment-Original.webm?2d640220\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-05-01-TH01-Debug-bomb-alignment-Original.webm?1187beaa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-05-01-TH01-Debug-bomb-alignment-Original.webm?c80af461\" type=\"video/webm\"\u003eVideo of TH01's in-game debug output, demonstrating how the lack of alignment on the \u003ccode\u003ebomb\u003c/code\u003e variable can lead to seemingly wrong \u003ccode\u003etimer\u003c/code\u003e values. \u003ca href=\"/blog/static/video/zmbv/2023-05-01-TH01-Debug-bomb-alignment-Original.avi?eea24ad1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"31\" data-title=\"Shot fired\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.webp?4a635f5d\" preload=\"none\" controls data-title=\"Anniversary Edition\" loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"105\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.avi?92260736\"\u003e\u003csource src=\"/blog/static/video/av1/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.webm?a3bc4bfd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.webm?50ec252a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.webm?a585e0f0\" type=\"video/webm\"\u003eVideo of the in-game debug output in the TH01 P0239 Anniversary Edition build, demonstrating fixed alignment for the \u003ccode\u003ebomb\u003c/code\u003e variable. \u003ca href=\"/blog/static/video/zmbv/2023-05-01-TH01-Debug-bomb-alignment-Anniversary.avi?92260736\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"31\" data-title=\"Shot fired\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\tFinally, I've streamlined VRAM page access changes, which allowed me to\n\t\tconsistently replace ZUN's expensive function call with the optimal two\n\t\tinlined x86 instructions. Interestingly, this change alone removed\n\t\t2\u0026nbsp;KiB from the binary size, which is almost all of the difference\n\t\tbetween \u003ca href=\"/blog/2023-03-14\"\u003e📝 the P0234-1 release\u003c/a\u003e and this\n\t\tone. Let's see how much longer we can make each new release of\n\t\t\u003ccode\u003eANNIV.EXE\u003c/code\u003e smaller than the previous one.\n\t\u003c/p\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe final point, however, raised the question of what we're now going to do\n\tabout\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 a certain issue in the \u003cspan lang='ja'\u003e地﻿獄\u003c/span\u003e/Jigoku Bad Ending\u003c/a\u003e.\n\tZUN's original expensive way of switching the accessed VRAM page was the\n\tmain reason behind the lag frames on slower PC-98 systems, and\n\tsearch-replacing the respective function calls would immediately get us to\n\tthe optimized version shown in that blog post. But is this something we\n\tactually want? If we wanted to retain the lag, we could surely preserve that\n\tfunction just for this one instance…\u003cbr\u003e The discovery of this issue\n\tpredates the clear distinction between bloat, quirks, and bugs, so it makes\n\tsense to first classify what this issue even is. The distinction comes all\n\tdown to \u003ci\u003eobservability\u003c/i\u003e, which I defined as changes to rendered frames\n\tbetween explicitly defined frame boundaries. That alone would be enough to\n\tcategorize any cause behind lag frames as bloat, but it can't hurt to be\n\tmore explicit here.\n\u003c/p\u003e\u003cp\u003e\n\tTherefore, I now officially judge observability in terms of an infinitely\n\tfast PC-98 that can instantly render everything between two explicitly\n\tdefined frames, and will never add additional lag frames. If we plan to port\n\tthe games to faster architectures that aren't bottlenecked by disappointing\n\tblitter chips, this is the only reasonable assumption to make, in my\n\topinion: The minimum system requirements in the games' README files are\n\t\u003ci\u003eminimums\u003c/i\u003e, after all, not recommendations. Chasing the exact frame\n\tdrop behavior that ZUN must have experienced during the time he developed\n\tthese games can only be a guessing game at best, because how can we know\n\twhich PC-98 model ZUN actually developed the games on? There might even be\n\tmore than one model, especially when it comes to TH01 which had been in\n\tdevelopment for at least two years before ZUN first sold it. It's also not\n\tlike any current PC-98 emulator even claims to emulate the specific timing\n\tof any existing model, and I sure hope that nobody expects me to import a\n\tbunch of bulky obsolete hardware just to count dropped frames.\n\u003c/p\u003e\u003cp\u003e\n\tThat leaves the tearing, where it's much more obvious how it's a bug. On an\n\tinfinitely fast PC-98, the \u003ci lang='ja' style='color: red'\u003eド﻿カ﻿ー﻿ン\u003c/i\u003e\n\tframe would never be visible, and thus falls into the same category as the\n\t\u003ca href=\"/blog/2022-01-31\"\u003e📝 two unused animations in the Sariel fight\u003c/a\u003e.\n\tWith only a single unconditional 2-frame delay inside the animation loop, it\n\tbecomes clear that ZUN intended both frames of the animation to be displayed\n\tfor 2 frames each:\n\u003c/p\u003e\u003cfigure style=\"width: 320px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.webp?85a5affa\" preload=\"none\" controls loop width=\"320\" height=\"208\" data-fps=\"56.423132\" data-frame-count=\"34\" style=\"aspect-ratio: 320 / 208\" data-lossless=\"/blog/static/video/zmbv/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.avi?16fb21f4\"\u003e\u003csource src=\"/blog/static/video/av1/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.webm?385f649f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.webm?b815c123\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.webm?206e7a12\" type=\"video/webm\"\u003eVideo of the shake/boom effect during the TH01 Jigoku Bad Ending as shown in the P0239 Anniversary Edition build, matching ZUN's intentions by showing both images for two frames each, without tearing. \u003ca href=\"/blog/static/video/zmbv/2023-05-01-TH01-Jigoku-Bad-Ending-shake-boom-Anniversary.avi?16fb21f4\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eNo tearing, and 34 frames in total for the first of the two\n\tinstances of this animation.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ReC98/releases/tag/P0239\"\u003e\n\t\u003cimg src=\"/static/emoji-th01.png?a7a77a94\" alt=\":th01:\" width=\"24\" height=\"24\" \u003e TH01 Anniversary Edition, version P0239\u003c/a\u003e\n\t\u003ca class=\"download\" href=\"/blog/static/2023-05-01-th01-anniv.zip?be86cd81\" data-kb=\"115.1\"\u003e2023-05-01-th01-anniv.zip \u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Taking the oldest still undelivered push and working towards TH04\n\tposition independence in preparation for multilingual translations. The\n\tShuusou Gyoku OpenGL backend shouldn't take \u003ci\u003ethat\u003c/i\u003e much longer either,\n\tso I should have lots of stuff coming up in May afterward.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-05-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-05-01T23:56:59Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-03-30",
      "url": "https://rec98.nmlgc.net/blog/2023-03-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-05-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-03-30\"\u003e\u003ctime datetime=\"2023-03-30T13:23:29Z\"\u003e2023-03-30 13:23\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0235\"\u003eP0235\u003c/a\u003e\n\t\t\tTH02 RE (Stage tiles, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e7a9262...62c4b7f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0236\"\u003eP0236\u003c/a\u003e\n\t\t\tTH02 RE (Stage tiles, part 2/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/62c4b7f...7fa9038\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0237\"\u003eP0237\u003c/a\u003e\n\t\t\tTH02 RE (Spark structure + Point number popups + Bomb animation effects)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/7fa9038...c5e51e6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/stage\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The main scrolling portions of gameplay in TH02, TH04, and TH05. Mostly rendered using tile maps.\"\u003estage\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, TH02! Being the only game whose main binary hadn't seen any dedicated\n\tattention ever, we get to start the TH02-related blog posts at the very\n\tbeginning with the most foundational pieces of code. The stage tile system\n\tis the best place to start here: It not only blocks every entity that is\n\trendered on top of these tiles, but is curiously placed right next to\n\tmaster.lib code in TH02, and would need to be separated out into its own\n\ttranslation unit before we can do the same with all the master.lib\n\tfunctions.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#tiles-2023-03-30\"\u003eThe stage tile system in TH02, TH04, and TH05\u003c/a\u003e \u003cul\u003e\u003cli\u003e\u003ca href=\"#diffs-2023-03-30\"\u003eGame-specific differences\u003c/a\u003e \u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#unused-2023-03-30\"\u003eTH02's unused Stage 5 tile sections\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#scroll-2023-03-30\"\u003eTH02's implementation of vertical scrolling\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#mistakes-2023-03-30\"\u003eMistakes in hand-written ASM, and how to fix them\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#pos-2023-03-30\"\u003eTH02's unique sprite position system\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"tiles-2023-03-30\"\u003e\u003cp\u003e\n\tIn late 2018, I already RE'd\n\t\u003ca href=\"/blog/2018-12-30\"\u003e📝 TH04's and TH05's stage tile \timplementation\u003c/a\u003e, but haven't properly documented it on this\n\tblog yet, so this post is also going to include the details that are unique\n\tto those games. On a high level, the stage tile system works identically in\n\tall three games:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe tiles themselves are 16×16 pixels large, and a stage can use 100 of\n\tthem at the same time.\u003c/li\u003e\n\t\u003cli\u003eThe optimal way of blitting tiles would involve VRAM-to-VRAM copies\n\twithin the same page using the EGC, and that's exactly what the games do.\n\tAll tiles are stored on both VRAM pages within the rightmost 64×400 pixels\n\tof the screen just right next to the HUD, and you only don't see them\n\tbecause the games cover the same area in text RAM with black cells:\n\t\u003cfigure class=\"fullres pixelated\"\u003e\n\t\t\u003cimg src=\"/blog/static/2023-03-30-TH02-Stage-1-initial-with-tiles.png?8dd89418\" alt=\"\"\u003e\n\t\t\u003cfigcaption\u003eThe initial screen of TH02's Stage 1, with the tile source\n\t\tarea uncovered by filling the same area in text RAM with transparent\n\t\tcells instead of black ones. In TH02, this also reveals how the tile\n\t\tarea ends with a bunch of glitch tiles, tinted blue in the image. These\n\t\tare the result of ZUN unconditionally blitting 100 tile images every\n\t\ttime, regardless of how many are actually contained in an\n\t\t\u003ccode\u003e.MPN\u003c/code\u003e file. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\t\tThese glitch tiles are another good example of a \u003ca\n\t\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-landmine\"\u003eZUN\n\t\tlandmine\u003c/a\u003e. Their appearance is the result of reading heap memory\n\t\toutside allocated boundaries, which can easily cause segmentation faults\n\t\twhen porting the game to a system with virtual memory. Therefore, these\n\t\twould not just be removed in this game's Anniversary Edition, but on the\n\t\tmore conservative \u003ccode\u003edebloated\u003c/code\u003e branch as well. Since the game\n\t\tnever uses these tiles and you can't observe them unless you manipulate\n\t\ttext RAM from outside the confines of the game, it's not a \u003ca\n\t\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-bug\"\u003ebug\u003c/a\u003e\n\t\taccording to our definition.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\t\u003cli\u003eTo reduce the memory required for a map, tiles are arranged into fixed\n\tvertical \u003ci\u003esections\u003c/i\u003e of a game-specific constant size. \u003cfigure\u003e\u003cfigure\n\tclass=\"side_by_side small\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-0.png?63bbf7fa\" alt=\"Section 0 of TH02's STAGE0.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-1.png?e09e4e04\" alt=\"Section 1 of TH02's STAGE0.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-2.png?683e7af8\" alt=\"Section 2 of TH02's STAGE0.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-3.png?d4a3aa53\" alt=\"Section 3 of TH02's STAGE0.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-4.png?1c1f82fc\" alt=\"Section 4 of TH02's STAGE0.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE0.MAP-5.png?155ce2c4\" alt=\"Section 5 of TH02's STAGE0.MAP\"\u003e\u003c/figure\u003e\u003cfigcaption\u003e\n\t\tThe 6 24×8-tile sections defined in TH02's \u003ccode\u003eSTAGE0.MAP\u003c/code\u003e, in\n\t\treverse order compared to how they're defined in the file. Note the\n\t\tduplicated row at the top of the final section: The boss fight starts\n\t\tonce the game scrolled the last full row of tiles onto the top of the\n\t\t\u003ci\u003escreen\u003c/i\u003e, not the \u003ci\u003eplayfield\u003c/i\u003e. But since the PC-98 text chip\n\t\tcovers the top tile row of the screen with black cells, this final row\n\t\tis never visible, which effectively reduces a map's final tile section\n\t\tto 7 rows rather than 8.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eThe actual stage map then is simply a list of these tile sections,\n\tordered from the start/bottom to the top/end.\u003c/li\u003e\n\t\u003cli\u003eAny manipulation of specific tiles within the fixed tile sections has to\n\tbe hardcoded. An example can be found right in Stage 1, where the Shrine\n\tTank leaves track marks on the tiles it appears to drive over:\n\t\u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-30-TH02-Shrine-Tank-timeout.webp?30da6d10\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"2330\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-03-30-TH02-Shrine-Tank-timeout.avi?9061ed70\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-30-TH02-Shrine-Tank-timeout.webm?4e0681fd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-30-TH02-Shrine-Tank-timeout.webm?36f20c7e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-30-TH02-Shrine-Tank-timeout.webm?548f8b53\" type=\"video/webm\"\u003eVideo of timing out the TH02 Stage 1 midboss on Easy Mode, with regular stage enemies disabled. Shows off the track marks left by the Shrine Tank, as well as how the fight times out one tile row too late. \u003ca href=\"/blog/static/video/zmbv/2023-03-30-TH02-Shrine-Tank-timeout.avi?9061ed70\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"61\" data-title=\"First track mark\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"2173\" data-title=\"Last track mark\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003e\n\t\t\tThis video also shows off the two issues with Touhou's first-ever\n\t\t\tmidboss: The replaced tiles are rendered \u003ci\u003ebelow\u003c/i\u003e the midboss\n\t\t\tduring their first 4 frames, and maybe ZUN should have stopped the\n\t\t\ttile replacements one row before the timeout. The first one is\n\t\t\tclearly a bug, but it's not so clear-cut with the second one. I'd\n\t\t\tneed to look at the code to tell for sure whether it's a quirk or a\n\t\t\tbug.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp id=\"diffs-2023-03-30\"\u003e\n\tThe differences between the three games can best be summarized in a table:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"comparison\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th02.png?c6bfc44e\" alt=\":th02:\" width=\"24\" height=\"24\" \u003e TH02\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e TH05\u003c/th\u003e\n\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003cth\u003eTile image file extension\u003c/th\u003e\n\t\u003ctd colspan=\"3\"\u003e.MPN\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eTile section format\u003c/th\u003e\n\t\u003ctd colspan=\"3\"\u003e.MAP\u003c/td\u003e\n\u003c/tr\u003e\u003ctr class=\"hr\"\u003e\n\t\u003cth\u003eTile section order defined as part of\u003c/th\u003e\n\t\u003ctd\u003e.DT1\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003e.STD\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eTile section index format\u003c/th\u003e\n\t\u003ctd colspan=\"2\"\u003e0-based ID\u003c/td\u003e\n\t\u003ctd\u003e0-based ID × 2\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eTile image index format\u003c/th\u003e\n\t\u003ctd\u003eIndex between 0 and 100, 1 byte\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003eVRAM offset in tile source area, 2 bytes\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eScroll speed control\u003c/th\u003e\n\t\u003ctd\u003eHardcoded\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003ePart of the .STD format, defined per referenced tile\n\tsection\u003c/td\u003e\n\u003c/tr\u003e\u003ctr class=\"hr\"\u003e\n\t\u003cth\u003eRedraw granularity\u003c/th\u003e\n\t\u003ctd\u003eFull tiles (16×16)\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003eHalf tiles (16×8)\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eRows per tile section\u003c/th\u003e\n\t\u003ctd\u003e8\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003e5\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eMaximum number of tile sections\u003c/th\u003e\n\t\u003ctd\u003e16\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003e32\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eLowest number of tile sections used\u003c/th\u003e\n\t\u003ctd\u003e5 \u003csmall\u003e(Stage 3 / Extra)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e8 \u003csmall\u003e(Stage 6)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e\u003cspan class=\"hovertext\" title=\"excluding Stage 6, which uses a single dummy section\"\u003e11 \u003csmall\u003e(Stage 2 / 4)\u003c/small\u003e\u003c/span\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr class=\"hr\"\u003e\n\t\u003cth\u003eHighest number of tile sections used\u003c/th\u003e\n\t\u003ctd\u003e13 \u003csmall\u003e(Stage 4)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e19 \u003csmall\u003e(Extra)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e24 \u003csmall\u003e(Stage 3)\u003c/small\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eMaximum length of a map\u003c/th\u003e\n\t\u003ctd\u003e320 sections (static buffer)\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003e256 sections (format limitation)\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eShortest map\u003c/th\u003e\n\t\u003ctd\u003e14 sections \u003csmall\u003e(Stage 5)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e20 sections \u003csmall\u003e(Stage 5)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e\u003cspan class=\"hovertext\" title=\"excluding Stage 6 with its dummy length of 10 sections\"\u003e15 sections \u003csmall\u003e(Stage 2)\u003c/small\u003e\u003c/span\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eLongest map\u003c/th\u003e\n\t\u003ctd\u003e143 sections \u003csmall\u003e(Stage 4)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e95 sections \u003csmall\u003e(Stage 4)\u003c/small\u003e\u003c/td\u003e\n\t\u003ctd\u003e40 sections \u003csmall\u003e(Stage 1 / 4 / Extra)\u003c/small\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003c/figure\u003e\u003chr id=\"unused-2023-03-30\"\u003e\u003cp\u003e\n\tThe most interesting part about stage tiles is probably the fact that some\n\tof the .MAP files contain \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e tile sections. 👀 Many\n\tof these are empty, duplicates, or don't really make sense, but a few\n\t\u003ci\u003eare\u003c/i\u003e unique, fit naturally into their respective stage, and might have\n\tbeen part of the map during development. In TH02, we can find three unused\n\tsections in Stage 5:\n\u003c/p\u003e\u003cfigure\u003e\u003cfigure class=\"side_by_side small\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-0.png?371436f3\" alt=\"Section 0 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-1.png?c3259229\" alt=\"Section 1 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-2.png?9285c18f\" alt=\"Section 2 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-3.png?c7deb9fa\" alt=\"Section 3 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-4.png?7a6e6fe9\" alt=\"Section 4 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-5.png?b0101c7f\" alt=\"Section 5 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-6.png?758a33e6\" alt=\"Section 6 of TH02's STAGE4.MAP\"\u003e\u003cimg src=\"/blog/static/2023-03-30-TH02-STAGE4.MAP-7.png?004c34ca\" alt=\"Section 7 of TH02's STAGE4.MAP\"\u003e\u003c/figure\u003e\u003cfigcaption\u003e\n\tThe non-empty tile sections defined in TH02's \u003ccode\u003eSTAGE4.MAP\u003c/code\u003e,\n\tshowing off three unused ones.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tThese unused tile sections are much more common in the later games though,\n\twhere we can find them in TH04's Stage 3, 4, and 5, and TH05's Stage 1, 2,\n\tand 4. I'll document those once I get to finalize the tile rendering code of\n\tthese games, to leave some more content for that blog post. TH04/TH05 tile\n\tcode would be quite an effective investment of your money in general, as\n\tmost of it is identical across both games. Or how about going for a full-on\n\tPC-98 Touhou map viewer and editor GUI?\n\u003c/p\u003e\u003chr id=\"scroll-2023-03-30\"\u003e\u003cp\u003e\n\tCompared to TH04 and TH05, TH02's stage tile code definitely feels like ZUN\n\twas just starting to understand how to pull off smooth vertical scrolling on\n\ta PC-98. As such, it comes with a few inefficiencies and suboptimal\n\timplementation choices:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe redraw flag for each tile is stored in a 24×25 \u003ccode\u003ebool\u003c/code\u003e\n\tarray that does nothing with 7 of the 8 bits. \u003c/li\u003e\n\t\u003cli\u003eDuring bombs and the Stage 4, 5, and Extra bosses, the game disables the\n\ttile system to render more elaborate backgrounds, which require the\n\tplayfield to be flood-filled with a single color on every frame. ZUN uses\n\tthe GRCG's RMW mode rather than TDW mode for this, leaving \u003cspan\n\tclass=\"hovertext\" title=\"Source: PC-9801 Programmers' Bible\"\u003ealmost half of\n\tthe potential performance\u003c/span\u003e on the table for no reason. Literally,\n\tchanging modes only involves changing a single constant.\u003c/li\u003e\n\t\u003cli\u003eThe scroll speed could theoretically be changed at any time. However,\n\tthe function that scrolls in new stage tiles can only ever blit part of a\n\t\u003ci\u003esingle\u003c/i\u003e tile row during every call, so it's up to the caller to ensure\n\tthat scrolling always ends up on an exact 16-pixel boundary. TH02 avoids\n\tthis problem by keeping the scroll speed constant across a stage, using 2\n\tpixels for Stage 4 and 1 pixel everywhere else.\u003c/li\u003e\n\t\u003cli\u003eSince the scroll speed is given in pixels, the slowest speed would be 1\n\tpixel per frame. To allow the even slower speeds seen in the final game,\n\tTH02 adds a separate scroll \u003ci\u003einterval\u003c/i\u003e variable that only runs the\n\tscroll function every 𝑛th frame, effectively adding a prescaler to the\n\tscroll speed. In TH04 and TH05, the speed is specified as a Q12.4 value\n\tinstead, allowing true fractional speeds at any multiple of\n\t\u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e16\u003c/sub\u003e pixels. This also necessitated a fixed algorithm\n\tthat correctly blits tile lines from two rows.\u003c/li\u003e\n\t\u003cli\u003eFinally, we've got a few inconsistencies in the way the code handles the\n\ttwo VRAM pages, which cause a few unnecessary tiles to be rendered to just\n\tone of the two pages. Mentioning that just in case someone tries to play\n\tthis game with a fully cleared text RAM and wonders where the flickering\n\ttiles come from.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tEven though this was ZUN's first attempt at scrolling tiles, he already saw\n\tit fit to write most of the code in assembly. This was probably a reaction\n\tto all of TH01's performance issues, and the frame rate reduction\n\tworkarounds he implemented to keep the game from slowing down too much in\n\tbusy places. \"If TH01 was all C++ and slow, TH02 better contain more ASM\n\tcode, and then it will be fast, right?\" \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAnother reason for going with ASM might be found in the kind of\n\tdocumentation that may have been available to ZUN. Last year, the PC-98\n\tcommunity discovered and scanned two new game programming tutorial books\n\tfrom 1991 (\u003ca\n\thref=\"https://archive.org/details/pc-9801-ASM-game-programming\"\u003e1\u003c/a\u003e, \u003ca\n\thref=\"https://archive.org/details/pc-9801-game-graphics-pc-9801\"\u003e2\u003c/a\u003e).\n\tTheir example code is not only entirely written in assembly, but restricts\n\titself to the bare minimum of x86 instructions that were available on the\n\t8086 CPU used by the original PC-9801 model 9 years earlier. Such code is\n\tnot only \u003ca\n\thref=\"https://archive.gamedev.net/archive/reference/articles/article369.html\"\u003esuboptimal\u003c/a\u003e\n\ton the 486, but can often be actually \u003ci\u003eworse\u003c/i\u003e than what your C++\n\tcompiler would generate. TH02 is where the trend of bad hand-written ASM\n\tcode started, and it\n\t\u003ca href=\"/blog/2021-02-21\"\u003e📝 only intensified in ZUN's later games\u003c/a\u003e. So,\n\tdon't copy code from these books unless you absolutely want to target the\n\tearlier 8086 and 286 models. Which,\n\t\u003ca href=\"/blog/2023-03-05#blitperf-2023-03-05\"\u003e📝 as we've gathered from the recent blitting benchmark results\u003c/a\u003e,\n\tare not all too common among current real-hardware owners.\u003cbr\u003e\n\tThat said, all that ASM code really only impacts readability and\n\tmaintainability. Apart from the aforementioned issues, the algorithms\n\tthemselves are mostly fine – especially since most EGC and GRCG operations\n\tare decently batched this time around, in contrast to TH01.\n\u003c/p\u003e\u003chr id=\"mistakes-2023-03-30\"\u003e\u003cp\u003e\n\tLuckily, the tile functions merely use \u003ci\u003einline\u003c/i\u003e assembly within a\n\ttypical C function and can therefore be at least part of a C++ source file,\n\teven if the result is pretty ugly. This time, we can actually be sure that\n\tthey weren't written directly in a .ASM file, because they feature x86\n\tinstruction encodings that can only be generated with Turbo C++ 4.0J's\n\tinline assembler, not with TASM. The same can't unfortunately be said about\n\tthe following function in the same segment, which marks the tiles covered by\n\tthe spark sprites \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhQAAIAPABAOy8qv///yH5BAUKAAEALAAAAABAAAgAAAJIjGGJye28nnygxuMu0hOrwE2h1yyRCZHeqaJgplhnVV6HDdIQrr/5LavxhsIMceVqqXwhlu/zZC6jTtIIBqV2rtFS5zsF53QFADs=\"\n\t\u003e for redrawing. In this one, it took just one dumb hand-written ASM\n\tinconsistency in the function's epilog to make the entire function\n\tundecompilable.\u003cbr\u003e\n\tThe standard x86 instruction sequence to set up a stack frame in a function prolog looks like this:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cpre\u003ePUSH\tBP\nMOV \tBP, SP\nSUB \tSP, ?? ; if the function needs the stack for local variables\u003c/pre\u003e\n\u003cfigcaption\u003eWhen compiling without optimizations, Turbo C++ 4.0J will\n\treplace this sequence with a single \u003ccode\u003eENTER\u003c/code\u003e instruction. That one\n\tis two bytes smaller, but much slower on every x86 CPU except for the 80186\n\twhere it was introduced.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIn functions without local variables, \u003ccode\u003eBP\u003c/code\u003e and \u003ccode\u003eSP\u003c/code\u003e\n\tremain identical, and a single \u003ccode\u003ePOP BP\u003c/code\u003e is all that's needed in\n\tthe epilog to tear down such a stack frame before returning from the\n\tfunction. Otherwise, the function needs an additional \u003ccode\u003eMOV SP,\n\tBP\u003c/code\u003e instruction to pop all local variables. With x86 being the helpful\n\tCISC architecture that it is, the 80186 also introduced the\n\t\u003ccode\u003eLEAVE\u003c/code\u003e instruction to perform both tasks. Unlike\n\t\u003ccode\u003eENTER\u003c/code\u003e, this single instruction\n\t\u003ci\u003eis\u003c/i\u003e faster than the raw two instructions on a lot of x86 CPUs (and\n\teven current ones!), and it's always smaller, taking up just 1 byte instead\n\tof 3.\u003cbr\u003e So what if you use \u003ccode\u003eLEAVE\u003c/code\u003e even if your function\n\t\u003ci\u003edoesn't\u003c/i\u003e use local variables? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e The fact that the\n\tinstruction first does the equivalent of \u003ccode\u003eMOV SP, BP\u003c/code\u003e doesn't\n\tmatter if these registers are identical, and who cares about the additional\n\tCPU cycles of \u003ccode\u003eLEAVE\u003c/code\u003e compared to just \u003ccode\u003ePOP BP\u003c/code\u003e,\n\tright? So that's definitely something you \u003ci\u003ecould theoretically\u003c/i\u003e do, but\n\tnot something that any compiler would ever generate.\n\u003c/p\u003e\u003cp\u003e\n\tAnd so, TH02 \u003ccode\u003eMAIN.EXE\u003c/code\u003e decompilation already hits the first\n\tbrick wall after two pushes. Awesome! \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e Theoretically,\n\twe \u003ci\u003ecould\u003c/i\u003e slowly mash through this wall using the \u003ca href=\"/blog/2023-01-17\"\u003e📝 code generator\u003c/a\u003e. But having such an inconsistency in the\n\tfunction epilog would mean that we'd have to keep Turbo C++ 4.0J from\n\temitting \u003ci\u003eany\u003c/i\u003e epilog \u003ci\u003eor\u003c/i\u003e prolog code so that we can write our\n\town. This means that we'd once again have to hide any use of the\n\t\u003ccode\u003eSI\u003c/code\u003e and \u003ccode\u003eDI\u003c/code\u003e registers from the compiler… and doing\n\t\u003ci\u003ethat\u003c/i\u003e requires code generation macros for 22 of the 49 instructions of\n\tthe function in question, almost none of which we currently have. So, this\n\tgets quite silly quite fast, \u003ci\u003eespecially\u003c/i\u003e if we only need to do it\n\t\u003ci\u003efor one single byte\u003c/i\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tInstead, wouldn't it be much better if we had a separate build step between\n\tcompile and link time that allowed us to replicate mistakes like these by\n\tjust patching the compiled .OBJ files? These files still contain the names\n\tof exported functions for linking, which would allow us to look up the code\n\tof a function in a robust manner, navigate to specific instructions using a\n\tdisassembler, replace them, and write the modified .OBJ back to disk before\n\tlinking. Such a system could then naturally expand to cover all other\n\tdecompilation issues, culminating in a full-on optimizer that could even\n\trecreate ZUN's self-modifying code. At that point, we would have sealed away\n\tall of ZUN's ugly ASM code within a separate build step, and could finally\n\tdecompile everything into readable C++.\n\u003c/p\u003e\u003cp\u003e\n\tPulling that off would require a significant tooling investment though.\n\tPatching that one byte in TH02's spark invalidation function could be done\n\twithin 1 or 2 pushes, but that's just one issue, and we currently have 32\n\tother .ASM files with undecompilable code. Also, note that this is\n\tfundamentally different from what we're doing with the\n\t\u003ccode\u003edebloated\u003c/code\u003e branch and the Anniversary Editions. Mistake patching\n\twould purely be about having readable code on \u003ccode\u003emaster\u003c/code\u003e that\n\tcompiles into ZUN's exact binaries, without fixing \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#categories\"\u003eweird\n\tcode\u003c/a\u003e. The Anniversary Editions go much further and rewrite such code in\n\ta much more fundamental way, improving it further than mistake patching ever\n\tcould.\u003cbr\u003e\n\tRight now, the Anniversary Editions seem \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1630306777391673348\"\u003emuch more\n\tpopular\u003c/a\u003e, which suggests that people just want 100% RE as fast as\n\tpossible so that I can start working on them. In that case, why bother with\n\tsuch undecompilable functions, and not just leave them in raw and unreadable\n\tx86 opcode form if necessary… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e But let's first\n\tsee how much backer support there actually is for mistake patching before\n\tfalling back on that.\n\u003c/p\u003e\u003cp\u003e\n\tThe best part though: Once we've made a decision and then covered TH02's\n\tspark and particle systems, \u003ci\u003ethat was it, and we will have already RE'd\n\tall ZUN-written PC-98-specific blitting code in this game\u003c/i\u003e. Every further\n\tsprite or shape is rendered via master.lib, and is thus decently abstracted.\n\tGuess I'll need to update\n\t\u003ca href=\"/blog/2022-08-15\"\u003e📝 the assessment of which PC-98 Touhou game is the easiest to port\u003c/a\u003e,\n\tbecause it sure isn't TH01, as we've seen with all the work required for the first Anniversary Edition build.\n\u003c/p\u003e\u003chr id=\"pos-2023-03-30\"\u003e\u003cp\u003e\n\tUntil then, there are still enough parts of the game that don't use any of\n\tthe remaining few functions in the \u003ccode\u003e_TEXT\u003c/code\u003e segment. Previously, I\n\tmentioned in the \u003ca href=\"/blog/2021-05-13\"\u003e📝 status overview blog post\u003c/a\u003e\n\tthat TH02 had a seemingly weird sprite system, but the spark and point popup\n\t(\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhYAAIAPABAAAAAP///yH5BAUKAAEALAAAAABgAAgAAAJkjI+pGwAMo5wUuVfduddw3XwWtkWkV4VWKXGtejajG9Ly7dlYDOJo0mvtRKxJ7IfbAY/AZq1jgvqGUsjxRBqONp+qL2pa/bDIZxGlzJYX6qht20W210Yuc7m8C+Psrj4FyHVQAAA7\"\n\talt=\"〇一二三四五六七八九十×÷\"\u003e) structures showed that the game just\n\tstores the current and previous position of its entities in a slightly\n\tdifferent way compared to the rest of PC-98 Touhou. Instead of having\n\tdedicated structure fields, TH02 uses two-element arrays indexed with the\n\tactive VRAM page. Same thing, and such a pattern even helps during RE since\n\tit's easy to spot once you know what to look for.\u003cbr\u003e\n\tThere's not much to criticize about the point popup system, except for maybe\n\ta landmine that causes sprite glitches when trying to display more than\n\t99,990 points. Sadly, the final push in this delivery was rounded out by yet\n\tanother piece of code at the opposite end of the quality spectrum. The\n\tparticle and smear effects for Reimu's bomb animations consist almost\n\tentirely of assembly bloat, which would just be replaced with generic calls\n\tto the generic blitter in this game's future Anniversary Edition.\n\u003c/p\u003e\u003cp\u003e\n\tIf I continue to decompile TH02 while avoiding the brick wall, items would\n\tbe next, but they probably require two pushes. Next up, therefore:\n\tIntegrating Stripe as an alternative payment provider into the order form.\n\tThere have been at least three people who reported issues with PayPal, and\n\tStripe has been working much better in tests. In the meantime, \u003ca\n\thref=\"https://buy.stripe.com/dR65o0eu497CdvGcMM\"\u003ehere's a temporary Stripe\n\torder link for everyone\u003c/a\u003e. This one is not connected to the cap yet, so\n\tplease make sure to stay within whatever value is currently shown on the\n\tfront page – I \u003ci\u003ewill\u003c/i\u003e treat any excess money as donations.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e If there's some time left afterward, I might\n\talso add some small improvements to the TH01 Anniversary Edition.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-05-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-03-30T13:23:29Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-03-14",
      "url": "https://rec98.nmlgc.net/blog/2023-03-14",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-03-14\"\u003e\u003ctime datetime=\"2023-03-14T15:00:00Z\"\u003e2023-03-14\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/policy-bugfix\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Examples of bugfixes in mod releases that fell under my free bugfix policy.\"\u003epolicy-bugfix\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/anniversary-edition\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Polished and bugfixed forks of 100% decompiled games.\"\u003eanniversary-edition\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sariel\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 魔界/Makai route.\"\u003esariel\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTurns out I was not \u003ci\u003equite\u003c/i\u003e done with the TH01 Anniversary Edition yet.\n\tYou might have noticed some white streaks at the beginning of Sariel's\n\tsecond form, which are in fact a bug that I accidentally added to the\n\tinitial release. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThese can be traced back to a \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#zun-quirk\"\u003equirk\u003c/a\u003e\n\tI wasn't aware of, and hadn't documented so far. When defeating Sariel's\n\tfirst form during a pattern that spawns pellets, it's likely for the second\n\tform to start with additional pellets that resemble the previous pattern,\n\tbut come out of seemingly nowhere. This shouldn't really happen if you look\n\tat the code: Nothing outside the typical pattern code spawns new pellets,\n\tand all existing ones are reset before the form transition…\n\u003c/p\u003e\u003cp\u003e\n\t\u003ci\u003eExcept\u003c/i\u003e if they're currently showing the 10-frame delay cloud\n\tanimation \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hRLR3kgU09GVCBHSUYgRW5jb2RlcgAh+QQJHgABACwAAAAAEAAQAAACJ4wNqceR7UB7MB4pq9tZo+0xVIh0l1lC40myLddmmIetKmpblbIwBQAh+QQFCgAAACwIAAgAAQABAAACAkQBACH5BAkeAAEALAAAAAAQABAAAAIfjB8AyLb8YDsxTneztTInr3EhBYok6Jkd2iiTmjxcAQAh+QQFCgAAACwIAAgAAQABAAACAkQBADs=\"\n\talt=\"\"\u003e, activated for all pellets during the symmetrical radial 2-ring\n\tpattern in Phase 2 and left activated for the rest of the fight. These\n\tpellets will continue their animation after the transition to the second\n\tform, and turn into regular pellets you have to dodge once their animation\n\tcompleted.\n\u003c/p\u003e\u003cp\u003e\n\tBy itself, this is just one more quirk to keep in mind during refactoring.\n\tIt only turned into a bug in the Anniversary Edition because the game tracks\n\tthe number of living pellets in a separate counter variable. After resetting\n\tall pellets, this counter is simply set to 0, regardless of any delay cloud\n\tpellets that may still be alive, and it's merely incremented or decremented\n\twhen pellets are spawned or leave the playfield.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIn the original game, this counter is only used as an optimization to skip\n\tspawning new pellets once the cap is reached. But with batched\n\tEGC-accelerated unblitting, it also makes sense to skip the rather costly\n\tsetup and shutdown of the EGC if no pellets are active anyway. Except if the\n\tcounter you use to check for that case can be 0 even if there \u003ci\u003eare\u003c/i\u003e\n\tpellets alive, which consequently don't get unblitted…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tThere is an optimal fix though: Instead of unconditionally resetting the\n\tliving pellet counter to 0, we decrement it for every pellet that\n\t\u003ci\u003edoes\u003c/i\u003e get reset. This preserves the quirk \u003ci\u003eand\u003c/i\u003e gives us a\n\tconsistently correct counter, allowing us to still skip every unnecessary\n\tloop over the pellet array.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tCutting out the lengthy defeat animation makes it easier to see where the\n\tadditional pellets come from.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tCutting out the lengthy defeat animation makes it easier to see where the\n\tadditional pellets come from. Also, note how regular unblitting resumes\n\t\tonce the first pellet gets clipped at the top of the playfield – the\n\t\tliving pellet counter then gets decremented to -1, and who uses\n\t\t\u003ccode\u003e\u0026lt;=\u003c/code\u003e rather than \u003ccode\u003e==\u003c/code\u003e on a seemingly unsigned\n\t\tcounter, right?\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tCutting out the lengthy defeat animation makes it easier to see where the\n\tadditional pellets come from.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.webp?ea515318\" preload=\"none\" controls data-title=\"Original game\" loop width=\"640\" height=\"336\" data-fps=\"20\" data-frame-count=\"153\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.avi?1bba41a3\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.webm?9a7a394c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.webm?b383d92d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.webm?13ec646b\" type=\"video/webm\"\u003eVideo of pellet delay clouds being carried over from TH01 Sariel's first form to her second form, in the original game. \u003ca href=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-Original.avi?1bba41a3\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Second form\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.webp?ea515318\" preload=\"none\" controls data-title=\"Anniversary Edition, P0234\" loop data-active width=\"640\" height=\"336\" data-fps=\"20\" data-frame-count=\"153\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.avi?e29741bb\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.webm?7452f57d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.webm?497876a5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.webm?5e0ebb14\" type=\"video/webm\"\u003eVideo of pellet delay clouds being carried over from TH01 Sariel's first form to her second form, in the initial P0234 release of the TH01 Anniversary Edition, demonstrating an accidental lack of unblitting. \u003ca href=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234.avi?e29741bb\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Second form\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"86\" data-title=\"Unblitting resumes\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.webp?ea515318\" preload=\"none\" controls data-title=\"Anniversary Edition, P0234-1\" loop width=\"640\" height=\"336\" data-fps=\"20\" data-frame-count=\"153\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.avi?e28cba40\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.webm?8b6989e4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.webm?ab88ff73\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.webm?85dae851\" type=\"video/webm\"\u003eVideo of pellet delay clouds being carried over from TH01 Sariel's first form to her second form, in the P0234-1 release of the TH01 Anniversary Edition, fixing the bug from the previous version. \u003ca href=\"/blog/static/video/zmbv/2023-03-14-TH01-Sariel-delay-cloud-carry-P0234-1.avi?e28cba40\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"63\" data-title=\"Second form\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tUltimately, this was a harmless bug that didn't affect gameplay, but it's\n\tstill something that players would have probably reported a few more times.\n\tSo here's a free bugfix:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca\n\t\tclass=\"release\"\n\t\thref=\"https://github.com/nmlgc/ReC98/releases/tag/P0234-1\"\n\t\u003e\u003cimg src=\"/static/emoji-th01.png?a7a77a94\" alt=\":th01:\" width=\"24\" height=\"24\" \u003e TH01 Anniversary Edition, version P0234-1\u003c/a\u003e \u003ca class=\"download\" href=\"/blog/static/2023-03-14-th01-anniv.zip?e1160e22\" data-kb=\"112.9\"\u003e2023-03-14-th01-anniv.zip \u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tThanks to mu021 for reporting this issue and providing helpful videos to\n\tidentify the cause!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-03-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-03-14T16:00:00+01:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-03-05",
      "url": "https://rec98.nmlgc.net/blog/2023-03-05",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-01-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-03-05\"\u003e\u003ctime datetime=\"2023-03-05T12:56:15Z\"\u003e2023-03-05 12:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0229\"\u003eP0229\u003c/a\u003e\n\t\t\tTH01 debloating (Single-executable build, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6370f96...d535d87\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0230\"\u003eP0230\u003c/a\u003e\n\t\t\tTH01 debloating (Single-executable build, part 2/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d535d87...ca523b4\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0231\"\u003eP0231\u003c/a\u003e\n\t\t\tResearch (Spawning TSRs from C)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ca523b4...05a49b9\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0232\"\u003eP0232\u003c/a\u003e\n\t\t\tPortability (PC-98 platform layer, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f7ef7f8...abeaf85\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0233\"\u003eP0233\u003c/a\u003e\n\t\t\tResearch (Performance of various PC-98 blitting approaches)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/abeaf85...dbc5b51\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0234\"\u003eP0234\u003c/a\u003e\n\t\t\tTH01 Anniversary Edition (Removing interlaced pellet rendering + Merging previous fixes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dd2265c...12f29c6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debloating\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Cleaning up ZUN\u0026#39;s original code into something you can actually read and maintain.\"\u003edebloating\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/anniversary-edition\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Polished and bugfixed forks of 100% decompiled games.\"\u003eanniversary-edition\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t.our-2023-03-05 {\n\t\tcolor: green;\n\t}\n\n\t.borland-2023-03-05 {\n\t\tcolor: red;\n\t}\n\n\t#blitperf-results-2023-03-05 {\n\t\tfont-size: 75%;\n\t}\n\n\t#blitperf-results-2023-03-05 thead {\n\t\tborder-bottom: var(--table-border);\n\t\tborder-bottom-width: 2px;\n\t}\n\n\t#blitperf-results-2023-03-05 thead td,\n\t#blitperf-results-2023-03-05 thead th {\n\t\tborder-right-width: 2px;\n\t}\n\n\t#blitperf-results-2023-03-05 tbody .b {\n\t\tborder-right-width: 2px;\n\t}\n\n\t#blitperf-results-2023-03-05 tr.fourplane {\n\t\tbackground-color: rgb(256, 220, 256);\n\t}\n\n\t#blitperf-results-2023-03-05 td,\n\t#blitperf-results-2023-03-05 th {\n\t\tborder: var(--table-border);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\t128 commits! Who would have thought that the ideal first release of the TH01\n\tAnniversary Edition would involve so much maintenance, and raise so many\n\tresearch questions? It's almost as if the real work only starts \u003ci\u003eafter\u003c/i\u003e\n\tthe 100% finalization mark… Once again, I had to steal some funding from the\n\treserved JIS trail word pushes to cover everything I liked to research,\n\twhich means that the next \u003cscript\u003eformatCurrency(15000)\u003c/script\u003e\u003cnoscript\u003e150.00\u0026nbsp;€\u003c/noscript\u003e towards the\n\t\u003cq\u003eanything\u003c/q\u003e goal will repay this debt. Luckily, this doesn't affect any\n\timmediate plans, as I'll be spending March with tasks that are already fully\n\tfunded.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#single-2023-03-05\"\u003eCombining TH01's three executables into a single one\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#mdrv98-2023-03-05\"\u003eStarting MDRV98 from within the game binary\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#landmines-2023-03-05\"\u003eBuffer overflows in packfile decompression (and an entire new distinction of weird code)\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#glitch-2023-03-05\"\u003ePreserving the negative glitch stages with a different binary layout\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#blitperf-2023-03-05\"\u003eBenchmarking and optimizing unaligned blitting of small sprites\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#egc-2023-03-05\"\u003eOptimizing EGC-powered unblitting\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#summary-2023-03-05\"\u003eSummary and future work\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"single-2023-03-05\"\u003e\u003cp\u003e\n\tSo, how did this end up so massive? The list of things I originally set out\n\tto do was pretty short:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eBuild entire game into single executable\u003c/li\u003e\n\t\u003cli\u003eFix rendering issues in the one or two most important parts of the game\n\tfor a good initial impression\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tBut even the first point already started with tons of little cleanup\n\tcommits. A part of them can definitely be blamed on the rush to hit the 100%\n\tdecompilation mark before the 25\u003csup\u003eth\u003c/sup\u003e anniversary last August.\n\tHowever, all the structural changes that I can't commit to\n\t\u003ccode\u003emaster\u003c/code\u003e reveal how much of a mess the TH01 codebase actually\n\tis.\u003cbr\u003e\n\tMerging the executables is mainly difficult because of all the\n\tinconsistencies between \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e and \u003ccode\u003eFUUIN.EXE\u003c/code\u003e.\n\tThe worst parts can be found in the \u003ccode\u003eREYHI*.DAT\u003c/code\u003e format code and\n\tthe High Score menu, but the little things are just as annoying, like how\n\tthe current \u003ccode\u003escore\u003c/code\u003e is an unsigned variable in\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, but a signed one in \u003ccode\u003eFUUIN.EXE\u003c/code\u003e.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e If it takes \u003ci\u003eme\u003c/i\u003e this long and this many\n\tcommits just to sort out all of these issues, it's no wonder that the only\n\tthing I've seen being done with this codebase since TH01's 100%\n\tdecompilation was a single porting attempt that ended in a rather quick\n\tragequit.\u003cbr\u003e\n\tSo why are we merging the executables in preparation for the Anniversary\n\tEdition, and not waiting with it until we start doing ports?\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eDistributing and updating one executable is cleaner than doing the same\n\twith three, especially as long as installation will still involve manually\n\tdropping the new binary into the game directory.\u003c/li\u003e\n\t\u003cli\u003eThe Anniversary Edition won't be the only fork binary. We are already\n\tgoing to start out with a separate \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e that contains\n\tonly the bloat removal changes without any bug fixes, and \u003ca\n\thref=\"https://twitter.com/spaztron64/status/1630749841096671233\"\u003espaztron64\n\twill probably redo his seizure-less edition\u003c/a\u003e. We don't want to clutter\n\tthe game directory with three binaries for each of these fork builds, and we\n\tespecially don't want to remember things like \u003cq\u003eoh, but \u003ci\u003ethis\u003c/i\u003e fork\n\tonly modifies \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e\u003c/q\u003e…\u003c/li\u003e\n\t\u003cli\u003eAll forks should run side-by-side with the original game. During the\n\ttime I was maintaining thcrap, I've had countless bug reports of people\n\tassuming that \u003cspan class=\"hovertext\" title=\"In fact, that distinction between thcrap bugs and game bugs is the origin of the 'ZUN bug' classification, which I\u0026#39;ve carried over to ReC98.\"\u003ethcrap was\n\tresponsible for bugs that were present in the original game\u003c/span\u003e, and the\n\tsame is certain to happen with the Anniversary Edition. Separate binaries\n\twill make it easier for everyone to check where these bugs came from.\u003c/li\u003e\n\t\u003cli\u003eAlso, I'd like to make a point about \u003ci\u003ehow\u003c/i\u003e bloated the original\n\tthree-executable structure really is, since I've heard people defending it\n\tas neat software architecture. Really, even in Real Mode where you typically\n\twant to use as little of the 640 KiB of conventional memory as possible, you\n\t\u003ci\u003edon't\u003c/i\u003e want to split your game up like this.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe game actually is \u003ci\u003eso\u003c/i\u003e bloated that the combined binary ended up\n\tsmaller than the original \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e. If all you see are the\n\tfile sizes of the original three executables, this might \u003ca\n\thref=\"https://twitter.com/Tasos500/status/1626321988925915137\"\u003elook like a\n\tpretty impressive feat\u003c/a\u003e. Like, how can we \u003ci\u003epossibly\u003c/i\u003e get 407,812\n\tbytes into less than 238,612 bytes, without using compression?\u003cbr\u003e\n\tIf you've ever looked at the linker map though, it's not at all surprising.\n\tExcluding the aforementioned inconsistencies that are hard to quantify,\n\t\u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eFUUIN.EXE\u003c/code\u003e only feature 5,767 and 6,475\n\tbytes of unique code and data, respectively. All other code in these\n\tbinaries is already part of \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, with more than half of\n\tthe size coming from the Borland C++ runtime. The single worst offender here\n\tis the C++ exception handler that Borland \u003ca\n\thref=\"https://github.com/nmlgc/Slices/blob/81787e49bccfc6bf22689d7aa4f14c9d25df4b48/Turbo%20C%2B%2B%204.02J%20(1994)/c0.asm#L258-L262\"\u003eforces\n\tonto every non-.COM binary by default\u003c/a\u003e, which alone adds 20,512 bytes\n\teven if your binary doesn't use C++ exceptions.\u003cbr\u003e\n\tOn a more hilarious note, \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/6370f96d9a5850ea71e4c12d7cc3f1d45e8c9d73/th01/hardware/graph.cpp#L550\"\u003ethis\n\tsingle line\u003c/a\u003e is responsible for pulling another unnecessary 14,242 bytes\n\tinto \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eFUUIN.EXE\u003c/code\u003e. This floating-point\n\tmultiplication is completely unnecessary in this context because all\n\tpossible parameters are integers, but it's enough for Turbo C++ and TLINK to\n\tpull in the entire x87 FPU emulation machinery. These two binaries don't\n\teven \u003ci\u003edraw\u003c/i\u003e lines, but since this function is part of the general\n\tgraphics code translation unit and contains other functions that these\n\tbinaries \u003ci\u003edo\u003c/i\u003e need, TLINK links in the entire thing. Maybe, multiple\n\texecutables aren't the best choice either if you use a linker that can't do\n\tdead code elimination…\n\u003c/p\u003e\u003cp\u003e\n\tSince the \u003ca href=\"/blog/2020-06-13\"\u003e📝 Orb's physics\u003c/a\u003e do turn the entire\n\tprecision of a \u003ccode\u003edouble\u003c/code\u003e variable into gameplay effects, it's not\n\tfeasible to ever get rid of all FPU code in TH01. The exception handler,\n\thowever, \u003ca\n\thref=\"https://community.embarcadero.com/article/technical-articles/162/14700\"\u003ecan\n\tbe removed\u003c/a\u003e, which easily brings the combined binary below the size of\n\tthe original \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e. Compiling all code with a single set\n\tof compiler optimization flags, including the more x86-friendly\n\t\u003ccode\u003epascal\u003c/code\u003e calling convention, then gets us a few more KB on top.\n\tAs does, of course, removing unused code: The only remaining purpose of\n\tfeatures such as \u003ca href=\"/blog/2020-01-05\"\u003e📝 resident palettes\u003c/a\u003e is to\n\tpotentially make porting more difficult for anyone who doesn't immediately\n\trealize that nothing in the game uses these functions.\u003cbr\u003e\n\tTechnically, \u003ci\u003eall\u003c/i\u003e unused code would be bloat, but for now, I'm keeping\n\tthe parts that may tell stories about the game's development history (such\n\tas unused effects or the \u003ca href=\"/blog/2022-08-11\"\u003e📝 mouse cursor\u003c/a\u003e), or\n\tthat might help with debugging. Even with that in mind, I've only scratched\n\tthe surface when it comes to bloat removal, and the binary is only going to\n\tget smaller from here. \u003ci\u003eA lot smaller.\u003c/i\u003e\n\u003c/p\u003e\u003cp\u003e\n\tIf only we now could start MDRV98 from this new combined binary, we wouldn't\n\tneed a second batch file either…\n\u003c/p\u003e\u003chr id=\"mdrv98-2023-03-05\"\u003e\u003cp\u003e\n\tWhich brings us to the first big research question of this delivery. Using\n\tthe C \u003ccode\u003espawn()\u003c/code\u003e function works fine on this compiler, so\n\t\u003ccode\u003espawn(\"MDRV98.COM\")\u003c/code\u003e would be all we need to do, right? Except\n\tthat the game crashes very soon after that subprocess returned.\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tSo it's not going to be \u003ci\u003ethat\u003c/i\u003e easy if the spawned process is a \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Terminate-and-stay-resident_program\"\u003eTSR\u003c/a\u003e.\n\tBut why should this be a problem? Let's take a look at the DOS heap, and how\n\tDOS lays out processes in conventional memory if we launch the game\n\tregularly through \u003ccode\u003eGAME.BAT\u003c/code\u003e:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2023-03-05-DOS-heap-batch.svg?14dae2a8\"\u003e\n\t\u003cfigcaption\u003eThe rough layout of the DOS heap when launching TH01 from\n\t\u003ccode\u003eGAME.BAT\u003c/code\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe batch file starts MDRV98 first, which will therefore end up \u003ci\u003ebelow\u003c/i\u003e\n\tthe game in conventional memory. This is perfect for a TSR: The program can\n\tresize itself arbitrarily before returning to DOS, and the rest of memory\n\twill be left over for the game. If we assume such a layout, a DOS program\n\tcan implement a custom memory allocator in a very simple way, as it only has\n\tto search for free memory in one direction – and this is exactly how Borland\n\timplemented the C heap for functions like \u003ccode\u003emalloc()\u003c/code\u003e and\n\t\u003ccode\u003efree()\u003c/code\u003e, and the C++ \u003ccode\u003enew\u003c/code\u003e and \u003ccode\u003edelete\u003c/code\u003e\n\toperators.\u003cbr\u003e\n\tBut if we spawn MDRV98 \u003ci\u003eafter\u003c/i\u003e starting TH01, well…\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2023-03-05-DOS-heap-MDRV98-adjacent.svg?6588e5df\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tMDRV98 will spawn in the next free memory location, allocate itself, return\n\tto TH01… which suddenly finds its C heap blocked from growing. As a result,\n\tthe next big allocation will immediately fail with a rather misleading \"out\n\tof memory\" error.\n\u003c/p\u003e\u003cp\u003e\n\tSo, what can we do about this? Still in a bloat removal mindset, my gut\n\treaction was to just throw out Borland's C heap implementation, and replace\n\tit with a very thin wrapper around the DOS heap as managed by \u003ccode\u003eINT 21h,\n\tAH=\u003ca href=\"https://stanislavs.org/helppc/int_21-48.html\"\u003e48h\u003c/a\u003e/\u003ca\n\thref=\"https://stanislavs.org/helppc/int_21-49.html\"\u003e49h\u003c/a\u003e/\u003ca\n\thref=\"https://stanislavs.org/helppc/int_21-4a.html\"\u003e4Ah\u003c/a\u003e\u003c/code\u003e. Like, \u003ca\n\thref=\"https://forum.vcfed.org/index.php?threads/can-somebody-please-explain-how-dos-memory-allocation-works.58785/#post-797612\"\u003ewhy\n\tdid these DOS compilers even bother with a custom allocator in the first\n\tplace if DOS already comes with a perfectly fine native one\u003c/a\u003e? Using the\n\tnative allocator would completely erase the distinction between TSR memory\n\tand game memory, and inherently allow the game to allocate beyond\n\tMDRV98.\u003cbr\u003e\n\tI did in fact implement this, and noticed even more benefits:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWhile DOS uses 16 bytes rather than Borland's 4 bytes for the control\n\tstructure of each memory block, this larger size automatically aligns all\n\tallocations to 16-byte boundaries. Therefore, all allocation addresses would\n\tfit into 16-bit segment-only pointers rather than needing 32-bit\n\t\u003ccode\u003efar\u003c/code\u003e ones. On the Borland heap, the 4-byte header further limits\n\tregular \u003ccode\u003efar\u003c/code\u003e pointers to 65,532 bytes, forcing you into\n\texpensive \u003ccode\u003ehuge\u003c/code\u003e pointers for bigger allocations.\u003c/li\u003e\n\t\u003cli\u003eDebuggers in DOS emulators typically have features to show and manage\n\tthe DOS heap. No need for custom debugging code.\u003c/li\u003e\n\t\u003cli\u003eYou can change the \u003ca\n\thref=\"https://stanislavs.org/helppc/int_21-58.html\"\u003ememory placement\n\tstrategy\u003c/a\u003e to allocate from the top of conventional memory down to the\n\tbottom. This is how the games allocate their resident structures.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUltimately though, the drawbacks became too significant. Most of them are\n\trelated to the PC-98 Touhou games only ever creating a single DOS\n\t\u003ci\u003eprocess\u003c/i\u003e, even though they contain multiple \u003ci\u003eexecutables\u003c/i\u003e.\n\tSwitching executables is done via \u003ccode\u003eexec()\u003c/code\u003e, which resizes a\n\tprogram's main allocation to match the new binary and then overwrites the\n\told program image with the new one. If you've ever wondered why DOSBox-X\n\tonly ever shows \u003ccode\u003eOP\u003c/code\u003e as the active process name in the title bar,\n\tyou now know why. As far as DOS is concerned, it's still the same\n\t\u003ccode\u003eOP.EXE\u003c/code\u003e process rooted at the same segment, and\n\t\u003ccode\u003eexec()\u003c/code\u003e doesn't bother rewriting the name either. Most\n\timportantly though, this is how \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e can launch into\n\tanother \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e process even if there are less than 238,612\n\tbytes free when \u003ccode\u003eexec()\u003c/code\u003e is called, and without consuming more\n\tmemory for every successive binary.\u003cbr\u003e\n\tFor now, \u003ccode\u003eANNIV.EXE\u003c/code\u003e still re-\u003ccode\u003eexec()\u003c/code\u003es itself at\n\tevery point where the original game did, as ZUN's original code really\n\tdepends on being reinitialized at boss and scene boundaries. The resulting\n\taccidental semi-\u003cq\u003ehot reloading\u003c/q\u003e is also a useful property to retain\n\tduring development.\u003cbr\u003e\n\tSo why is the DOS heap a bad idea for regular game allocation after all?\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eEven DOS automatically releases all memory associated with a process\n\tduring its termination. But since we keep running the same process until the\n\tplayer quits out of the main menu, we lose the C heap's implicit cleanup on\n\t\u003ccode\u003eexec()\u003c/code\u003e, and have to manually free all memory ourselves.\u003c/li\u003e\n\t\u003cli\u003eSince the binary can be larger after hot reloading, we in fact \u003ci\u003ehave\n\tto\u003c/i\u003e allocate all regular memory using the \u003ci\u003elast fit\u003c/i\u003e strategy.\n\tOtherwise, \u003ccode\u003eexec()\u003c/code\u003e fails to resize the program's main block for\n\tthe same reason that crashed the game on our initial attempt to\n\t\u003ccode\u003espawn(\"MDRV98.COM\")\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eJust like Borland's heap implementation, the DOS heap stores its control\n\tstructures immediately before each allocation, forming a singly linked list.\n\tBut since the entire OS shares this single list, corruptions from heap\n\toverflows also affect the whole system, and become much more disastrous.\n\tTheoretically, it might be possible to recover from them by forcibly\n\treleasing all blocks after the last correct one, or even by doing a\n\tbrute-force search for valid \u003ca\n\thref=\"https://stanislavs.org/helppc/memory_control_block.html\"\u003ememory\n\tcontrol blocks\u003c/a\u003e, but in reality, DOS will likely just throw error code #7\n\t(\u003ccode\u003eERROR_ARENA_TRASHED\u003c/code\u003e) on the next memory management syscall,\n\tforcing a reboot.\u003cbr\u003e\n\tWith a custom allocator, small corruptions remain isolated to the process.\n\tThey can be even further limited if the process adds some padding between\n\tits last internal allocation and the end of the allocated DOS memory block;\n\tBorland's heap sort of does this as well by always rounding up the DOS block\n\tto a full KiB. All this might not make a difference in today's emulated and\n\tsingle-tasked usage, but would have back then when software was still\n\tdeveloped inside IDEs running on the same system.\u003c/li\u003e\n\t\u003cli\u003eTH01's debug mode uses \u003ccode\u003eheapcheck()\u003c/code\u003e and\n\t\u003ccode\u003eheapchecknode()\u003c/code\u003e, and reimplementing these on top of the DOS\n\theap is not trivial. On the contrary, it would be the most complicated part\n\tof such a wrapper, by far.\u003c/li\u003e\n\t\u003cli\u003eFinally, and most importantly for TH01 in particular: The observable\n\teffects of the\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 test/debug mode HP bar heap corruption glitches\u003c/a\u003e\n\tare a direct result of Borland's C heap implementation.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tI could release this DOS heap wrapper in unused form for another push if\n\tanyone's interested, but for now, I'm pretty happy with not actually using\n\tit in the games. Instead, let's stay with the Borland C heap, and find a way\n\tto push MDRV98 to the very top of conventional RAM. Like this:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2023-03-05-DOS-heap-MDRV98-top.svg?6c99910b\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhich is much easier said than done. It would be nice if we could just use\n\tthe \u003ci\u003elast fit\u003c/i\u003e allocation strategy here, but .COM executables always\n\treceive all free memory by default anyway, which eliminates any difference\n\tbetween the strategies.\u003cbr\u003e\n\tBut we can still change \u003ci\u003ememory\u003c/i\u003e itself. So let's temporarily claim all\n\tremaining free memory, minus the exact amount we need for MDRV98, for our\n\tprocess. Then, the only remaining free space to spawn MDRV98 is at the exact\n\tplace where we want it to be:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cembed src=\"/blog/static/2023-03-05-DOS-heap-before-MDRV98-top.svg?62e816f7\"\u003e\n\t\u003cfigcaption\u003e\n\t\tObviously, we release all the additional memory after spawning MDRV98.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow we only need to know how much memory to not temporarily allocate. First,\n\twe need to replicate the assumption that MDRV98's \u003ccode\u003e-M7\u003c/code\u003e\n\tcommand-line parameter corresponds to a resident size of 23,552 bytes. This\n\tis not as bad as it seems, because the \u003ccode\u003e-M\u003c/code\u003e parameter explicitly\n\thas a KiB unit, and we can nicely abstract it away for the API.\u003cbr\u003e\n\tThe \u003ci\u003e(env.)\u003c/i\u003e block though? Its minimum size equals the combined length\n\tof all environment variables passed to the process, but its maximum size is…\n\tnot limited at all?! As in, DOS implementations can add and have\n\thistorically added more free space because some programs insisted on storing\n\ttheir own new environment variables in this exact segment. DOSBox and\n\tDOSBox-X follow this tradition by providing a configuration option for the\n\tadditional amount of environment space, with the latter \u003ca\n\thref=\"https://github.com/joncampbell123/dosbox-x/commit/1a18812\"\u003eadding 1024\n\tadditional bytes by default, y'know, just in case someone wants to compile\n\tFreeDOS on a slow emulator\u003c/a\u003e. It's not even worth sending a bug report for\n\tthis specific case, because it's only a symptom of the fact that\n\tunexpectedly large program environment blocks can and will happen, and are\n\tto be expected in DOS land.\u003cbr\u003e\n\tSo thanks to this cruel joke, it's technically impossible to achieve what we\n\twant to do there. Hooray! The only thing we can kind of do here is an\n\teducated guess: Sum up the length of all environment variables in our\n\tenvironment block, compare that length against the allocated size of the\n\tblock, and assume that the MDRV98 process will get as much additional memory\n\tas our process got. 🤷\n\u003c/p\u003e\u003cp\u003e\n\tThe remaining hurdles came courtesy of some Borland C runtime implementation\n\tdetails. You would think that the temporary reallocation could even be done\n\tin pure C using the \u003ccode\u003esbrk()\u003c/code\u003e, \u003ccode\u003ecoreleft()\u003c/code\u003e, and\n\t\u003ccode\u003ebrk()\u003c/code\u003e functions, but all values passed to or returned from\n\tthese functions are inaccurate because they don't factor in the\n\taforementioned KiB padding to the underlying DOS memory block. So we have to\n\tdirectly use the DOS syscalls after all. Which at least means that learning\n\tabout them wasn't \u003ci\u003ecompletely\u003c/i\u003e useless…\u003cbr\u003e\n\tThe final issue is caused inside \u003cspan class=\"borland-2023-03-05\"\u003eBorland's\n\t\u003ccode\u003espawn()\u003c/code\u003e implementation\u003c/span\u003e. The environment block for the\n\tchild process is built out of all the strings reachable from C's\n\t\u003ccode\u003eenviron\u003c/code\u003e pointer, which is what that FreeDOS build process\n\t\u003ci\u003eshould\u003c/i\u003e have used. Coalescing them into a single buffer involves yet\n\tanother C heap allocation… and since we didn't report our DOS memory block\n\tmanipulation back to the C heap, the \u003ccode\u003emalloc()\u003c/code\u003e call might think\n\tit needs to request more memory from DOS. This resets the DOS memory block\n\tback to its intended level, undoing our manipulation right before the actual\n\t\u003ca href=\"https://stanislavs.org/helppc/int_21-4b.html\"\u003e\u003ccode\u003eINT 21h, AH=4Bh\n\tEXEC\u003c/code\u003e\u003c/a\u003e syscall. Or in short:\n\u003c/p\u003e\u003cblockquote\u003e\u003cspan class=\"our-2023-03-05\"\n\t\u003eManipulate DOS heap ➜ \u003ccode\u003espawn()\u003c/code\u003e call ➜\u003c/span\n\u003e \u003cspan class=\"borland-2023-03-05\"\n\t\u003e\u003ccode\u003e_LoadProg()\u003c/code\u003e ➜ allocate and prepare environment block ➜ \u003ccode\u003e_spawn()\u003c/code\u003e ➜ DOS \u003ccode\u003eEXEC\u003c/code\u003e syscall\u003c/span\n\u003e\u003c/blockquote\u003e\u003cp\u003e\n\tThe obvious solution: Replace \u003ccode\u003e_LoadProg()\u003c/code\u003e, implement the\n\tcoalescing ourselves, and do it before the heap manipulation. Fortunately,\n\tBorland's internal low-level \u003ccode\u003e_spawn()\u003c/code\u003e function is not\n\t\u003ccode\u003estatic\u003c/code\u003e, so we can call it ourselves whenever we want to:\n\u003c/p\u003e\u003cblockquote\u003e\u003cspan class=\"our-2023-03-05\"\n\t\u003eAllocate and prepare environment block ➜ manipulate DOS heap ➜ \u003ccode\u003e_spawn()\u003c/code\u003e call ➜\u003c/span\n\u003e \u003cspan class=\"borland-2023-03-05\"\n\t\u003e\u003ccode\u003eEXEC\u003c/code\u003e syscall\u003c/span\n\u003e\u003c/blockquote\u003e\u003cp\u003e\n\tSo yes, launching MDRV98 from C \u003ci\u003ecan\u003c/i\u003e be done, but it involves advanced\n\twitchcraft and is completely ridiculous. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tLaunching external sound drivers from a batch file \u003ci\u003eis\u003c/i\u003e the right way\n\tof doing things.\u003cbr\u003e\n\tFortunately, you don't have to rely on this auto-launching feature. You can\n\tstill launch \u003ccode\u003eDEBLOAT.EXE\u003c/code\u003e or \u003ccode\u003eANNIV.EXE\u003c/code\u003e from a batch\n\tfile that launched \u003ccode\u003eMDRV98.COM\u003c/code\u003e before, and the binaries will\n\tdetect this case and skip the attempt of launching MDRV98 from C. It's\n\tunlikely that my heuristic will ever break, but I definitely recommend\n\treplicating \u003ccode\u003eGAME.BAT\u003c/code\u003e just to be completely sure – especially\n\tfor user-friendly repacks that don't want to include the original game\n\tanyway.\u003cbr\u003e\n\tThis is also why \u003ccode\u003eANNIV.EXE\u003c/code\u003e doesn't launch\n\t\u003ccode\u003eZUNSOFT.COM\u003c/code\u003e: The \"correct\" and stable way to launch\n\t\u003ccode\u003eANNIV.EXE\u003c/code\u003e still involves a batch file, and I would say that\n\texpecting people to remove \u003ccode\u003eZUNSOFT.COM\u003c/code\u003e from that file is worse\n\tthan not playing the animation. It's certainly a debate we can have, though.\n\u003c/p\u003e\u003chr id=\"landmines-2023-03-05\"\u003e\u003cp\u003e\n\tThis deep dive into memory allocation revealed another previously\n\tundocumented bug in the original game. The RLE decompression code for the\n\t\u003ccode\u003e東方靈異.伝\u003c/code\u003e packfile contains two heap overflows, which are\n\tactually triggered by SinGyoku's \u003ccode\u003eBOSS1_3.BOS\u003c/code\u003e and Konngara's\n\t\u003ccode\u003eBOSS8_1.BOS\u003c/code\u003e. They only do not immediately crash the game when\n\tloading these bosses thanks to two implementation details of Borland's C\n\theap. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tObviously, this is a bug we should fix, but according to the definition of\n\tbugs, that fix would be exclusive to the \u003ccode\u003eanniversary\u003c/code\u003e branch.\n\tIsn't that too restrictive for something this critical? This code is\n\tguaranteed to blow up with a different heap implementation, if only in a\n\tDebug build. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e And besides, nobody would notice a fix\n\tjust by looking at the game's rendered output…\n\u003c/p\u003e\u003cp\u003e\n\tLooks like we have to introduce a fourth category of weird code, in addition\n\tto the previous \u003ci\u003ebloat\u003c/i\u003e, \u003ci\u003ebug\u003c/i\u003e, and \u003ci\u003equirk\u003c/i\u003e categories, for\n\tinvisible internal issues like these. Let's call it \u003ci\u003elandmine\u003c/i\u003e, and fix\n\tthem on the \u003ccode\u003edebloated\u003c/code\u003e branch as well. \u003ca\n\thref=\"https://twitter.com/Clerish/status/1623990678937034752\"\u003eThanks to\n\tClerish for the naming inspiration\u003c/a\u003e!\u003cbr\u003e\n\tWith this new category, the full definitions for all categories have become\n\tquite extensive. Thus, they now live in \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#labeling-weird-or-broken-code\"\u003e\u003ccode\u003eCONTRIBUTING.md\u003c/code\u003e\n\tinside the ReC98 repository\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tWith the new discoveries and the new landmine category, TH01 is now at 67\n\tbugs and 20 landmines. And the solution for the landmine in question? \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/commit/30325e168d50c51ad4c1402c79c29b7633ddd672\"\u003eSimplifying\n\tthe 61 lines of the original code down to 16.\u003c/a\u003e And yes, I'm including\n\tcomments in these numbers – if the interactions of the code are complex\n\tenough to require multi-paragraph comments, these \u003ci\u003eare\u003c/i\u003e a necessary and\n\tvalid part of the code.\n\u003c/p\u003e\u003chr id=\"glitch-2023-03-05\"\u003e\u003cp\u003e\n\tWhile we're on the topic of weird code and its visible or invisible effects,\n\tthere's one thing you might be concerned about. With all the rearchitecting\n\tand data shifting we're doing on the \u003ccode\u003edebloated\u003c/code\u003e branch, what\n\twill happen to the \u003ca href=\"/blog/2022-08-14\"\u003e📝 negative glitch stages\u003c/a\u003e?\n\tThese are the result of a clearly observable bug that, by definition, must\n\tnot be fixed on the \u003ccode\u003edebloated\u003c/code\u003e branch. But given that the\n\tobservable layout of the glitch stages is defined by the memory\n\t\u003ci\u003esurrounding\u003c/i\u003e the scene stage variable, won't the\n\t\u003ccode\u003edebloated\u003c/code\u003e branch inherently alter their appearance (= ⚠️\n\tfanfiction ⚠️), or even remove them completely?\n\u003c/p\u003e\u003cp\u003e\n\tWell, yes, it will. But we can still preserve their layout by\n\t\u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/dd2265cf92022b1d7e48e0b3e701e11dd5995a67/th01/main/stage/stageobj.cpp#L30-L104\"\u003ehardcoding\n\tthe exact original data that the game would originally read\u003c/a\u003e, and even \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/dd2265cf92022b1d7e48e0b3e701e11dd5995a67/th01/main/stage/stageobj.cpp#L355-L370\"\u003eemulate\n\tthe original segment relocations and other pieces of global data\u003c/a\u003e.\u003cbr\u003e\n\tDoing this is feasible thanks to the fact that there are only 4 glitch\n\tstages. Unfortunately, the same can't be said for the timer values, which\n\tare determined by an array lookup with the un-modulo'd stage ID. If we\n\twanted to preserve those as well, we'd have to bundle an exact copy of the\n\toriginal \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e data segment to preserve the values of all\n\t32,768 negative stages you could possibly enter, \u003ci\u003etogether\u003c/i\u003e with a map\n\tof all relocations in this segment. 😵 Which I've decided against for now,\n\tsince this has been going on for far too long already. Let's first see \u003cspan\n\tclass=\"hovertext\" title=\"I'm aware that me mentioning it will make this much more likely!\"\u003eif\n\tanyone ever actually complains\u003c/span\u003e about details like this…\n\u003c/p\u003e\u003chr id=\"blitperf-2023-03-05\"\u003e\u003cp\u003e\n\tAlright, time to start the \u003ccode\u003eanniversary\u003c/code\u003e branch by rendering\n\teverything at its correct internal unaligned X position? Eh… maybe not quite\n\tyet. If we just hacked all the necessary bit-shifting code into all the\n\tformat-specific blitting functions, we'd still retain all this largely\n\tredundant, bad, and slow code, and would make no progress in terms of\n\tportability. It'd be much better to first write a single generic blitter\n\tthat's decently optimized, but supports all kinds of sprites to make this\n\toptimization actually worth something.\u003cbr\u003e\n\tSo, next research question: How would such a blitter look like? After I\n\tlearned during my\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 first foray into cycle counting\u003c/a\u003e that port\n\tI/O is slow on 486 CPUs, it became clear that TH04's\n\t\u003ca href=\"/blog/2020-04-03\"\u003e📝 GRCG batching for pellets\u003c/a\u003e was one of the\n\tmore useful optimizations that probably contributed a big deal towards\n\tachieving the high bullet counts of that game. This leads to two\n\tconclusions:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003emaster.lib's \u003ccode\u003esuper_*()\u003c/code\u003e sprite functions are slow, and not\n\tworth looking at for inspiration. Even the \u003ca href=\"/blog/2021-11-08\"\u003e📝 tiny format\u003c/a\u003e reinitializes the GRCG on every color change, wasting 80\n\tcycles.\u003c/li\u003e\n\t\u003cli\u003eHence, our low-level blitting API should not even care about colors. It\n\tshould only concern itself with blitting a given 1bpp sprite to a single\n\tVRAM segment. This way, it can work for both 4-plane sprites and\n\tsingle-plane sprites, and just assume that the GRCG is active.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tMaybe we should also start by not even doing these unaligned bit shifts\n\tourselves, and instead expect the call site to\n\t\u003ca href=\"/blog/2019-02-28\"\u003e📝 always deliver a byte-aligned sprite that is \tcorrectly preshifted\u003c/a\u003e,\n\tif necessary? Some day, we definitely should measure how slow runtime\n\tshifting would really be…\n\u003c/p\u003e\u003cp\u003e\n\tWhat we should do, however, are some further general optimizations that I\n\twould have expected from master.lib: \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Duff%27s_device\"\u003eUnrolling the vertical\n\tloop\u003c/a\u003e, and baking a single function for every sprite width to eliminate\n\tthe horizontal loop. We can then use the widest possible x86\n\t\u003ccode\u003eMOV\u003c/code\u003e instruction for the lowest possible number of cycles per\n\trow – for example, we'd blit a 56-wide sprite with three \u003ccode\u003eMOV\u003c/code\u003es\n\t(32-bit + 16-bit + 8-bit), and a 64-wide one with two 32-bit\n\t\u003ccode\u003eMOVs\u003c/code\u003e.\u003cbr\u003e\n\tOr maybe not? There's a lot of blitting code in both master.lib and PC-98\n\tTouhou that checks for empty bytes within sprites to skip needlessly writing\n\tthem to VRAM:\n\u003c/p\u003e\u003cpre\u003euint8_t left_half = ((uint8_t *)(sprite))[0];\nuint8_t right_half = ((uint8_t *)(sprite))[1];\nif(right_half != 0x00) {\n\tpokeb(VRAM_SEGMENT, (vram_offset + 0), left_half);\n}\nif(right_half != 0x00) {\n\tpokeb(VRAM_SEGMENT, (vram_offset + 1), right_half);\n}\u003c/pre\u003e\u003cp\u003e\n\tWhich goes against everything you seem to know about computers. We aren't\n\trunning on an 8-bit CPU here, so wouldn't it be faster to always write both\n\thalves of a sprite in a single operation?\n\u003c/p\u003e\u003cpre\u003euint16_t both_halves = ((uint16_t *)(sprite))[0];\npokew(VRAM_SEGMENT, vram_offset, both_halves);\n\u003c/pre\u003e\u003cp\u003e\n\tThat's a single CPU instruction, compared to two instructions and two\n\tbranches. The only possible explanation for this would be that VRAM writes\n\tare \u003ci\u003eso slow\u003c/i\u003e on PC-98 that you'd want to avoid them at all costs, even\n\tif that means additional branching on the CPU to do so. Or maybe that was\n\tsomething you would want to do on certain models with slow VRAM, but not on\n\tothers?\n\u003c/p\u003e\u003cp\u003e\n\tSo I wrote a benchmark to answer all these questions, and to compare my new\n\tblitter against typical TH01 blitting code:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-05-BLITPERF.webp?01bd0290\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"2079\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-03-05-BLITPERF.avi?1fb186e5\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-05-BLITPERF.webm?4a6fbf74\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-05-BLITPERF.webm?ecf1b8c7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-05-BLITPERF.webm?6c5eed0b\" type=\"video/webm\"\u003eVideo of BLIT386.EXE running on DOSBox-X. \u003ca href=\"/blog/static/video/zmbv/2023-03-05-BLITPERF.avi?1fb186e5\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tA not really representative run on DOSBox-X. Since the master.lib sprite\n\t\tfunctions are also unbatched, I expect them to not be much faster than\n\t\tthe naive C implementation.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t\u003ca class=\"download\" href=\"/blog/static/2023-03-05-blitperf.zip?15a637d3\" data-kb=\"32.2\"\u003e2023-03-05-blitperf.zip \u003c/a\u003e\n\tAnd here are the real-hardware results I've got from the \u003ci\u003ePC-9800\n\tCentral\u003c/i\u003e Discord server:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable id=\"blitperf-results-2023-03-05\" class=\"numbers\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth colspan=\"3\" rowspan=\"3\"\u003e\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-286LS\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9801ES\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9821Cb/Cx\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9821Ap3\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9821An\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9821Nw133\u003c/th\u003e\n\t\t\t\u003cth colspan=\"2\"\u003ePC-9821Ra20\u003c/th\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e80286, 12 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003ei386SX, 16 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e486SX, 33 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e486DX4, 100 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003ePentium, 90 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003ePentium, 133 MHz\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003ePentium Pro, 200 MHz\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1987\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1989\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1994\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1994\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1994\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1997\u003c/td\u003e\n\t\t\t\u003ctd colspan=\"2\"\u003e1996\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth rowspan=\"4\"\u003eUnchecked\u003c/th\u003e\n\t\t\t\u003cth\u003eC\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e36,85\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e38,42\u003c/td\u003e\n\t\t\t\u003ctd\u003e26,02\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e26,87\u003c/td\u003e\n\t\t\t\u003ctd\u003e3,98\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e4,13\u003c/td\u003e\n\t\t\t\u003ctd\u003e2,08\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e2,16\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,81\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,87\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,86\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,89\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,25\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,25\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003eMOVS\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e15,22\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e16,87\u003c/td\u003e\n\t\t\t\u003ctd\u003e9,33\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e10,19\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,22\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,37\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,44\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,44\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth rowspan=\"2\"\u003e\u003ccode\u003eMOV\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e15,42\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e17,08\u003c/td\u003e\n\t\t\t\u003ctd\u003e9,65\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e10,53\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,15\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,3\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,44\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,44\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr class=\"fourplane\"\u003e\n\t\t\t\u003cth class=\"b\"\u003e4-plane\u003c/th\u003e\n\t\t\t\u003ctd\u003e37,23\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e43,97\u003c/td\u003e\n\t\t\t\u003ctd\u003e29,2\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e32,96\u003c/td\u003e\n\t\t\t\u003ctd\u003e4,44\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,01\u003c/td\u003e\n\t\t\t\u003ctd\u003e4,39\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e4,67\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,11\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,32\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,61\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,74\u003c/td\u003e\n\t\t\t\u003ctd\u003e6,63\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e6,64\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth colspan=\"2\" rowspan=\"2\"\u003eChecking first\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e17,49\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e19,15\u003c/td\u003e\n\t\t\t\u003ctd\u003e10,84\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e11,72\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,27\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,44\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,04\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,07\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,54\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,54\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr class=\"fourplane\"\u003e\n\t\t\t\u003cth class=\"b\"\u003e4-plane\u003c/th\u003e\n\t\t\t\u003ctd\u003e46,49\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e53,36\u003c/td\u003e\n\t\t\t\u003ctd\u003e35,01\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e38,79\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,66\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e6,26\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,43\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,74\u003c/td\u003e\n\t\t\t\u003ctd\u003e6,56\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e6,8\u003c/td\u003e\n\t\t\t\u003ctd\u003e8,08\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e8,29\u003c/td\u003e\n\t\t\t\u003ctd\u003e10,25\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e10,29\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth colspan=\"2\" rowspan=\"2\"\u003eChecking second\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e16,47\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e18,12\u003c/td\u003e\n\t\t\t\u003ctd\u003e10,77\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e11,65\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,25\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,39\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,02\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,51\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,51\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr class=\"fourplane\"\u003e\n\t\t\t\u003cth class=\"b\"\u003e4-plane\u003c/th\u003e\n\t\t\t\u003ctd\u003e43,41\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e50,26\u003c/td\u003e\n\t\t\t\u003ctd\u003e33,79\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e37,82\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,22\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,81\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,14\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,43\u003c/td\u003e\n\t\t\t\u003ctd\u003e6,18\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e6,4\u003c/td\u003e\n\t\t\t\u003ctd\u003e7,57\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e7,77\u003c/td\u003e\n\t\t\t\u003ctd\u003e9,58\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e9,62\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\t\u003cth colspan=\"2\" rowspan=\"2\"\u003eChecking both\u003c/th\u003e\n\t\t\t\u003cth class=\"b\"\u003eGRCG\u003c/th\u003e\n\t\t\t\u003ctd\u003e16,14\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e18,03\u003c/td\u003e\n\t\t\t\u003ctd\u003e10,84\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e11,71\u003c/td\u003e\n\t\t\t\u003ctd\u003e1,33\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,49\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e1,01\u003c/td\u003e\n\t\t\t\u003ctd\u003e0,49\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e0,49\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\u003ctr class=\"fourplane\"\u003e\n\t\t\t\u003cth class=\"b\"\u003e4-plane\u003c/th\u003e\n\t\t\t\u003ctd\u003e43,61\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e50,45\u003c/td\u003e\n\t\t\t\u003ctd\u003e34,11\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e37,87\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,39\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,99\u003c/td\u003e\n\t\t\t\u003ctd\u003e4,92\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e5,23\u003c/td\u003e\n\t\t\t\u003ctd\u003e5,88\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e6,11\u003c/td\u003e\n\t\t\t\u003ctd\u003e7,19\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e7,43\u003c/td\u003e\n\t\t\t\u003ctd\u003e9,1\u003c/td\u003e\n\t\t\t\u003ctd class=\"b\"\u003e9,13\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tAmount of frames required to render 2000 16×8 pellet sprites on a variety of\n\tPC-98 models, using the new generic blitter. Both preshifted (first column)\n\tand runtime-shifted (second column) sprites were tested; empty columns\n\tcorrespond to times faster than a single frame. Thanks to cuba200611,\n\tShoutmon, cybermind, and Digmac for running the tests!\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe key takeaways:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eChecking for empty bytes has never been a good idea.\u003c/li\u003e\n\t\u003cli\u003ePreshifting sprites made a slight difference on the 286. Starting with\n\tthe 386 though, that difference got smaller and smaller, until it completely\n\tvanished on Pentium models. The memory tradeoff is especially not worth it\n\tfor 4-plane sprites, given that you would have to preshift each of the 4\n\tplanes and possibly even a fifth alpha plane. Ironically, ZUN only ever\n\tpreshifted monochrome single-bitplane sprites with a width of 8 pixels.\n\tThat's the smallest possible amount of memory a sprite can possibly take,\n\tand where preshifting consequently has the smallest effect on performance.\n\tShifting 8-wide sprites on the fly literally takes a single \u003ccode\u003eROL\u003c/code\u003e\n\tor \u003ccode\u003eROR\u003c/code\u003e instruction per row.\u003c/li\u003e\n\t\u003cli\u003eYou might want to use \u003ccode\u003eMOVS\u003c/code\u003e instead of \u003ccode\u003eMOV\u003c/code\u003e when\n\ttargeting the 286 and 386, but the performance gains are barely worth the\n\tresulting mess you would make out of your blitting code. On Pentium models,\n\tthere is no difference.\u003c/li\u003e\n\t\u003cli\u003eUse the GRCG whenever you have to render lots of things that share a\n\tstatic 8×1 pattern.\u003c/li\u003e\n\t\u003cli\u003eThese are the PC-98 models that the people who are willing to test your\n\tnewly written PC-98 code actually use.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSince this won't be the only piece of game-independent and explicitly\n\tPC-98-specific custom code involved in this delivery, it makes sense to\n\tstart \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/master/platform/x86real/pc98\"\u003ea\n\tdedicated PC-98 platform layer\u003c/a\u003e. This code will gradually eliminate the\n\tdependency on master.lib and replace it with better optimized and more\n\treadable C++ code. The blitting benchmark, for example, is already\n\timplemented completely without master.lib.\u003cbr\u003e\n\tWhile this platform layer is mainly written to generate optimal code within\n\tTurbo C++ 4.0J, it can also serve as general PC-98 documentation for\n\teveryone who prefers code over machine-translating old Japanese books. Not\n\tto mention the immediacy of having all actual \u003ci\u003erelevant\u003c/i\u003e information in\n\tone place, which might otherwise be pretty well hidden in these books, or\n\tsome obscure old text file. For example, did you know that uploading gaiji\n\tvia \u003ccode\u003eINT 18h\u003c/code\u003e might end up disabling the VSync interrupt trigger,\n\tdeadlocking the process on the next frame delay loop? This nuisance is not\n\treplicated by any emulators, and it's quite frustrating to encounter it when\n\ttrying to run your code on real hardware. master.lib works around it by\n\tsimply hooking \u003ccode\u003eINT 18h\u003c/code\u003e and unconditionally reenabling the VSync\n\tinterrupt trigger after the original handler returns, and so does our\n\tplatform layer.\n\u003c/p\u003e\u003chr id=\"egc-2023-03-05\"\u003e\u003cp\u003e\n\tSo, with the pellet draw calls batched and routed through the new renderer,\n\twe should have gained enough free CPU cycles to disable\n\t\u003ca href=\"/blog/2020-07-12\"\u003e📝 interlaced pellet rendering\u003c/a\u003e without any\n\timpact on frame rates?\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.webp?baa37c97\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"925\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.avi?baa0cdea\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.webm?c98bf811\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.webm?b2da860d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.webm?40438544\" type=\"video/webm\"\u003eVideo of my initial attempt to deinterlace TH01's pellet rendering, demonstrated with Kikuri's first pattern, showing noticeable and reproducible screen tearing. \u003ca href=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-deinterlace-attempt.avi?baa0cdea\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tWell, kinda. We do get 56.4 FPS, but only together with noticeable and\n\treproducible tearing in the top part of the playfield, suggesting exactly\n\t\u003ci\u003ewhy\u003c/i\u003e ZUN interlaced the rendering in the first place. 😕 So have we\n\talready reached the limit of single-buffered PC-98 games here, or can we\n\tstill do something about it?\u003cbr\u003e\n\tAs it turns out, the main bottleneck actually lies in the pellet\n\t\u003ci\u003eunblitting\u003c/i\u003e code. Every EGC-\"accelerated\" unblitting call in TH01 is\n\tas unbatched as the pellet blitting calls were, spending an additional 17\n\tI/O port writes per call to completely set up and shut down the EGC, every\n\ttime. And since this is TH01, the two-instruction operation of changing the\n\tactive PC-98 VRAM page isn't inlined either, but instead done via a function\n\tcall to a faraway segment. On the 486, that's:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u0026gt;341 cycles for EGC setup and teardown, plus\u003c/li\u003e\n\t\u003cli\u003e\u0026gt;72 cycles for each 16-pixel chunk to be unblitted.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003eThis sums up to\u003cul\u003e\n\t\u003cli\u003e\u0026gt;917 cycles of completely unnecessary work for every active pellet,\n\tin the \u003ci\u003eoptimal\u003c/i\u003e 50% of cases where it lies on an even VRAM byte,\n\tor\u003c/li\u003e\n\t\u003cli\u003e\u0026gt;1493 cycles if it lies on an odd VRAM byte, because ZUN's code\n\textends the unblitted rectangle to a gargantuan 32×8 pixels in this case\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd this calculation even ignores the lack of small micro-optimizations that\n\tcould further optimize the blitting loop. Multiply that by the game's pellet\n\tcap of 100, and we get \u003ci\u003ea 6-digit number of wasted CPU cycles\u003c/i\u003e. On\n\tpaper, that's roughly \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e6\u003c/sub\u003e of the time we have for each\n\tof our target 56.423 FPS on the game's target 33\u0026nbsp;MHz systems. Might not\n\tsound all too critical, but the single-buffered nature of the game means\n\tthat we're effectively racing the beam on every frame. In turn, we have to\n\tbe even more serious about performance.\n\u003c/p\u003e\u003cp\u003e\n\tSo, time to also add a batched EGC API to our PC-98 platform layer? Writing\n\tour own EGC code presents a nice opportunity to finally look deeper into \u003ca\n\thref=\"https://www.webtech.co.jp/company/doc/undocumented_mem/io_egc.txt\"\u003eall\n\tits registers and configuration options\u003c/a\u003e, and see what exactly we can do\n\tabout ZUN's enforced 16-pixel alignment.\u003cbr\u003e\n\tTo nobody's surprise, this alignment is completely unnecessary, and only\n\tdisplays a lack of knowledge about the chip. While it \u003ci\u003eis\u003c/i\u003e true that\n\tthe EGC wants VRAM to be exclusively addressed in 16-bit chunks at\n\t16-bit-aligned addresses, it specifically provides\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ean address register (\u003ccode\u003e0x4AC\u003c/code\u003e) for shifting the horizontal\n\tstart offsets of the source and destination to any pixel \u003ci\u003ewithin\u003c/i\u003e the\n\t16 pixels of such a chunk, and\u003c/li\u003e\n\t\u003cli\u003ea bit length register (\u003ccode\u003e0x4AE\u003c/code\u003e) for specifying the total\n\twidth of pixels to be transferred, which also implies the correct end\n\toffsets.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd it gets even better: After \u003ccode\u003e⌈bitlength\u0026nbsp;÷\u0026nbsp;16⌉\u003c/code\u003e write\n\tinstructions, the EGC's internal shifter state automatically reinitializes\n\titself in preparation for blitting another row of pixels with the same\n\tinitially configured bit addresses and length. This is perfect for blitting\n\trectangles, as two I/O port writes before the start of your blitting loop\n\tare enough to define your entire rectangle.\n\u003c/p\u003e\u003cp\u003e\n\tThe manual nature of reading and writing in 16-pixel chunks does come with a\n\tslight pitfall though. If the source bit address is larger than the\n\tdestination bit address, the first 16-bit read won't fill the EGC's internal\n\tshift register with all pixels that should appear in the first 16-pixel\n\tdestination chunk. In this case, the EGC simply won't write anything and\n\tleave the first chunk unchanged. In a\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 regular blitting loop\u003c/a\u003e, however, you expect\n\tthat memory to be written and immediately move on to the next chunks within\n\tthe row. As a result, the actual blitting process for such a rectangle will\n\tno longer be aligned to the configured address and bit length. The first row\n\tof the rectangle will appear 16 pixels to the right of the destination\n\taddress, and the second one will start at bit offset 0 with pixels from the\n\trightmost byte of the first line, which weren't blitted and remained in the\n\ttile register.\u003cbr\u003e\n\tThere is an easy solution though: Before the horizontal loop on each line of\n\tthe rectangle, simply read one additional 16-pixel chunk from the source\n\tlocation to prefill the shift register. Thankfully, it's large enough to\n\talso fit the second read of the then full 16 pixels, without dropping any\n\tpixels along the way.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's how we get arbitrarily unaligned rectangle copies with the EGC!\n\tExcept for a small register allocation trick to use two-register addressing,\n\tthere's not much use in further optimizations, as the runtime of these\n\tinter-page blit operations is dominated by the VRAM page switches anyway.\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-05-EGC.webp?500a0d13\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"1025\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-03-05-EGC.avi?3e1834fb\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-05-EGC.webm?c0f55a9b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-05-EGC.webm?a7420355\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-05-EGC.webm?26c6e503\" type=\"video/webm\"\u003eVideo of testing arbitrarily unaligned rectangle copies with the EGC. \u003ca href=\"/blog/static/video/zmbv/2023-03-05-EGC.avi?3e1834fb\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tExcept that T98-Next seems to disagree about the register prefilling issue:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\u003cimg\n\tsrc=\"/blog/static/2023-03-05-EGC-T98-Next.png?f34790d5\"\n\talt=\"Glitched blitting results on T98-Next when trying EGC copies where the source bit address is larger than the destination bit address\"\n\u003e\u003c/figure\u003e\u003cp\u003e\n\tEvery other emulator agrees with real hardware in this regard, so we can\n\tsafely assume this to be a bug in T98-Next. Just in case this old emulator\n\twith its last release from June 2010 still has any fans left nowadays… For\n\tnow though, even they can still enjoy the TH01 Anniversary Edition: The only\n\tEGC copy algorithm that TH01 actually needs is the left one during the\n\tsingle-buffered tests, which even \u003ci\u003ethat\u003c/i\u003e emulator gets right.\u003cbr\u003e\n\tThat only leaves\n\t\u003ca href=\"/blog/2019-11-06\"\u003e📝 my old offer of documenting the EGC raster ops\u003c/a\u003e,\n\tand we've got the EGC figured out completely!\n\u003c/p\u003e\u003chr id=\"summary-2023-03-05\"\u003e\u003cp\u003e\n\tAnd that did in fact remove tearing from the pellet rendering function! For\n\tthe first time, we can now fight Elis, Kikuri, Sariel, and Konngara with a\n\tdoubled pellet frame rate:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-05-TH01-pellet-rendering-Anniversary.webp?0774c1a5\" preload=\"none\" controls data-title=\"Anniversary Edition\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"925\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-Anniversary.avi?0045492e\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-05-TH01-pellet-rendering-Anniversary.webm?1a3e04f4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-05-TH01-pellet-rendering-Anniversary.webm?a5e0b2ec\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-05-TH01-pellet-rendering-Anniversary.webm?b92cc9f2\" type=\"video/webm\"\u003eVideo of TH01 Kikuri's first pattern in the first Anniversary Edition build, demonstrating pellet rendering at the full, non-interlaced frame rate. \u003ca href=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-Anniversary.avi?0045492e\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-03-05-TH01-pellet-rendering-Original.webp?0774c1a5\" preload=\"none\" controls data-title=\"Original game\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"925\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-Original.avi?7ea9d0ae\"\u003e\u003csource src=\"/blog/static/video/av1/2023-03-05-TH01-pellet-rendering-Original.webm?2689f6c2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-03-05-TH01-pellet-rendering-Original.webm?93de54cf\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-03-05-TH01-pellet-rendering-Original.webm?b5915133\" type=\"video/webm\"\u003eVideo of TH01 Kikuri's first pattern in ZUN's original build, demonstrating interlaced pellet rendering at a halved frame rate. \u003ca href=\"/blog/static/video/zmbv/2023-03-05-TH01-pellet-rendering-Original.avi?7ea9d0ae\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tSwitchable videos like these can nicely provide evidence that these\n\t\tchanges have no effect on gameplay, making it easy to see that the Orb\n\t\tstill collides with all pellets on the same frames. Also, check out the\n\t\tdifference in remaining conventional memory (\u003ccode\u003ecoreleft\u003c/code\u003e)…\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith only pellets and no other animation on screen, this exact pattern\n\tpresents the optimal demonstration case for the new unblitter. But as you\n\tcan already tell from the invincibility sprites, we'd also need to route\n\tevery other kind of sprite through the same new code. This isn't all too\n\ttrivial: Most sprites are still rendered at byte-aligned positions, and\n\ttheir blitting APIs hide that fact by taking a pixel position regardless.\n\tThis is why we can't just replace ZUN's original 16-pixel-aligned EGC\n\tunblitting function with ours, and always have to replace both the blitter\n\t\u003ci\u003eand\u003c/i\u003e the unblitter on a per-sprite basis.\u003cbr\u003e\n\tTo completely remove all flickering, we'd also like to get rid of all the\n\tsprite-specific unblit\u0026nbsp;➜ update\u0026nbsp;➜ render sequences, and instead\n\tgather all unblitting code to the beginning of the game loop, before any\n\tupdate and rendering calls. So yeah, it will take a long time to completely\n\tget rid of all flickering. Until we're there, I recommend any backer to tell\n\tme their favorite boss, so that I can focus on getting \u003ci\u003ethat\u003c/i\u003e one\n\trendered without any flickering. Remember that here at ReC98, we can have a\n\tTouhou character popularity contest at any time during the year, whenever\n\tthe store is open! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tIn the meantime, the consistent use of 8×8 rectangles during pellet\n\tunblitting does significantly reduce flickering across the entire game,\n\tand shrinks certain holes that pellets tend to rip into lazily reblitted\n\tsprites:\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-03-05-TH01-SinGyoku-crossing-pellet-unblitting-Anniversary.png?96033871\"\n\t\tdata-title=\"Anniversary Edition\"\n\t\talt=\"TH01 SinGyoku's crossing pellet pattern in the Anniversary Edition, demonstrating smaller unblitting artifacts\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2023-03-05-TH01-SinGyoku-crossing-pellet-unblitting-Original.png?eff5cc14\"\n\t\tdata-title=\"Original game\"\n\t\talt=\"The same frame in the original game, featuring much more giant holes ripped into the sphere sprite\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003eSinGyoku's \"crossing pellets\" pattern, shortly before completing\n\tthe transformation back to the sphere.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tTo round out the first release, I added all the other bug fixes to achieve\n\tparity with my previously released patched \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e builds:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eI removed the \u003ca href=\"/blog/2022-05-31\"\u003e📝 shootout laser crash\u003c/a\u003e by\n\tsimply leaving the lasers on screen if a boss is defeated,\u003c/li\u003e\n\t\u003cli\u003eprevented the HP bar heap corruption bug in test or debug mode by not\n\tletting it display negative HP in the first place, and\u003c/li\u003e\n\t\u003cli\u003erestored \u003ca href=\"/blog/2022-01-31\"\u003e📝 the two animations during the Sariel fight that were lost to type confusion errors in the original game\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo here it is, the first build of TH01's Anniversary Edition:\n\t\u003ca class=\"download\" href=\"/blog/static/2023-03-05-th01-anniv.zip?50837342\" data-kb=\"111.7\"\u003e2023-03-05-th01-anniv.zip \u003c/a\u003e\n\t\u003cstrong\u003eEdit (2023-03-12): If you're playing on Neko Project and seeing more\n\tflickering than in the original game, make sure you've checked the \u003ci\u003eScreen\n\t→ Disp vsync\u003c/i\u003e option.\u003c/strong\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The long overdue extended trip through the depths of TH02's\n\tlow-level code. From what I've seen of it so far, the work on this project\n\tis finally going to become a bit more relaxing. Which is quite welcome\n\tafter, what, 6 months of stressful research-heavy work?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2023-01-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-03-05T12:56:15Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2023-01-17",
      "url": "https://rec98.nmlgc.net/blog/2023-01-17",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-12-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2023-01-17\"\u003e\u003ctime datetime=\"2023-01-17T17:52:09Z\"\u003e2023-01-17 17:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0227\"\u003eP0227\u003c/a\u003e\n\t\t\tTH05 decompilation (Sara) / Research (Relativity of near references)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4f85326...bfd24c6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0228\"\u003eP0228\u003c/a\u003e\n\t\t\tTH05 finalization (Lasers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/bfd24c6...739e1d8\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003enrook, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 1 boss.\"\u003esara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/laser\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Beams that can collide with the player. Sometimes difficult.\"\u003elaser\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tStarting the year with a delivery that \u003ci\u003ewasn't\u003c/i\u003e delayed until the last\n\tday of the month for once, nice! Still, \u003cq\u003every soon\u003c/q\u003e and\n\t\u003cq\u003ehigh-maintenance\u003c/q\u003e did \u003ci\u003enot\u003c/i\u003e go well together…\n\u003c/p\u003e\u003cp\u003e\n\tIt definitely wasn't Sara's fault though. As you would expect from a Stage 1\n\tBoss, her code was no challenge at all. Most of the TH02, TH04, and TH05\n\tbosses follow the same overall structure, so let's introduce a new table to\n\treplace most of the boilerplate overview text:\n\u003c/p\u003e\u003ctable class=\"boss_overview numbers\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\t\u003cth\u003ePhase #\u003c/th\u003e\n\t\u003cth\u003ePatterns\u003c/th\u003e\n\t\u003cth\u003eHP boundary\u003c/th\u003e\n\t\u003cth colspan=\"2\"\u003eTimeout condition\u003c/th\u003e\n\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003ctd rowspan=\"4\"\u003e\u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhQABAALMAAACA//z8/PzszOy4qOyouKio/MxUzPxERIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAABAAEAAAwT/EMhJq7046827/2AojmRpnmhqEkYLtAahzgBhw61t02gQ6EBdgFfy/VgxoZEoMvqQOaeM+XH+WolEVEf1BJGJQTZ363ZYOuxgPU6bOVpcNiyY496bMXYuqOtdeBoJAHNhA31sWlqBHCx0fYh2U4wZN48CYnE7lJWOhZ8xnBkwCQVzBaalBaGiFAGoirCoprOwg6JOqbWppaVGqYy5tgVWp6hOt2+/tVnHRsavyHjLr4XHhQjNxcpWtgnZnwji2bNzb4U+vOPrCAfu4MCUzdjtSj8H4OacvYXu7kHu5ohp9enbOH/vsqwZKMpQwTn+8iXa53DOOojjDDEMlAVTJoMI/7O8y7amDqMEHj+KQ8hS3KNkXTomssiyJck+MIkUwpRFAsiaETv6MSOUjc+VQBOixMmR560E+JImhDT0nD6RUpXiLHSOAtSs+B66zBkTbECD7Fxy/AoW7cGLeNhmzbYOYUayNORmzYK05lgqWM1+mwsOsFl/gwnjRRFYcDugRmwsNtFYMGQfESfN0NtWL+aWk0mwfdyW9IHPoHl87buXNGq72fL6YN3a3eugPTcfIEA7ak2oRlh+OhBaBFQbQE0jvg0RsU4EyG3+/g38bPERBqOPVJ40gQ+uTPhCJxCR9ux2Ls/GzY55PAEr+Maezm2V3RF224EHuJ7Xbdq0X9FHkQ9/AH7SilcPGXjgggyiEAEAOw==\"\n\t\talt=\"Sprite of Sara in TH05\"\n\t\u003e\u003c/td\u003e\n\t\u003cth\u003e(Entrance)\u003c/th\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\t\u003cth\u003e4,650\u003c/th\u003e\n\t\u003ctd\u003e288 frames\u003c/td\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003e2\u003c/th\u003e\n\t\u003ctd\u003e4\u003c/td\u003e\n\t\u003ctd\u003e2,550\u003c/td\u003e\n\t\u003ctd\u003e2,568 frames\u003c/td\u003e\n\t\u003ctd\u003e(= 32 patterns)\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003e3\u003c/th\u003e\n\t\u003ctd\u003e4\u003c/td\u003e\n\t\u003ctd\u003e450\u003c/td\u003e\n\t\u003ctd\u003e5,296 frames\u003c/td\u003e\n\t\u003ctd\u003e(= 24 patterns)\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003e4\u003c/th\u003e\n\t\u003ctd\u003e1\u003c/td\u003e\n\t\u003ctd\u003e0\u003c/td\u003e\n\t\u003ctd\u003e1,300 frames\u003c/td\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003ctfoot\u003e\u003ctr\u003e\n\t\u003cth\u003eTotal\u003c/th\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\t\u003cth\u003e9\u003c/th\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\t\u003cth\u003e9,452 frames\u003c/th\u003e\n\t\u003ctd\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003c/tfoot\u003e\u003c/table\u003e\u003cul\u003e\n\t\u003cli\u003eIn Phases 2 and 3, Sara cycles between waiting, moving randomly for a\n\tfixed 28 frames, and firing a random pattern among the 4 phase-specific\n\tones. The pattern selection makes sure to \u003cspan class=\"hovertext\"\n\ttitle=\"Since it's an infinite loop, a rogue RNG will cause the game to hang here, but that's a purely theoretical concern.\"\u003enever\u003c/span\u003e\n\tpick any pattern twice in a row. Both phases contain spiral patterns that\n\tonly differ in the clockwise or counterclockwise turning direction of the\n\tspawner; these directions are treated as individual unrelated patterns, so\n\tit's possible for the \"same\" pattern to be fired multiple times in a row\n\twith a flipped direction.\u003cbr\u003e\n\tThe two phases also differ in the wait and pattern durations:\n\t\u003cul\u003e\n\t\t\u003cli\u003eIn Phase 2, the wait time starts at 64 frames and decreases by 12\n\t\tframes after the first 5 patterns each, ending on a minimum of 4 frames.\n\t\tIn Phase 3, it's a constant 16 frames instead.\u003c/li\u003e\n\t\t\u003cli\u003eAll Phase 2 patterns are fired for 28 frames, after a 16-frame\n\t\tgather animation. The Phase 3 pattern time starts at 80 frames and\n\t\tincreases by 24 frames for the first 6 patterns, ending at 200 frames\n\t\tfor all later ones.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003ePhase 4 consists of the single laser corridor pattern with additional\n\trandom bullets every 16 frames.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd that's all the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e-relevant detail that ZUN put into Sara's code. It doesn't even make sense to describe the remaining\n\tpatterns in depth, as their groups can significantly change between\n\tdifficulties and rank values. The\n\t\u003ca href=\"/blog/2022-04-30\"\u003e📝 general code structure of TH05 bosses\u003c/a\u003e\n\twon't ever make for \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e, but Sara's code is just a\n\tlesser example of what I already documented for Shinki.\u003cbr\u003e\n\tSo, no bugs, no unused content, only inconsequential bloat to be found here,\n\tand less than 1 push to get it done… That makes 9 PC-98 Touhou bosses\n\tdecompiled, with 22 to go, and gets us over the sweet 50% overall\n\tfinalization mark! 🎉 And sure, it might be possible to pass through the\n\tlasers in Sara's final pattern, but the boss script just controls the\n\torigin, angle, and activity of lasers, so any quirk there would be part of\n\tthe laser code… wait, you can do \u003ci\u003ewhat\u003c/i\u003e?!?\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tTH05 expands TH04's one-off code for Yuuka's Master and Double Sparks into a\n\tmore featureful laser system, and Sara is the first boss to show it off.\n\tThus, it made sense to look at it again in more detail and finalize the code\n\tI had purportedly\n\t\u003ca href=\"/blog/2018-12-16\"\u003e📝 reverse-engineered over 4 years ago\u003c/a\u003e.\n\tThat very short delivery notice already hinted at a very time-consuming\n\tfuture finalization of this code, and that prediction certainly came true.\n\tOn the surface, \u003ci\u003eall\u003c/i\u003e of the low-level laser ray rendering and\n\tcollision detection code is undecompilable: It uses the \u003ccode\u003eSI\u003c/code\u003e and\n\t\u003ccode\u003eDI\u003c/code\u003e registers without Turbo C++'s safety backups on the stack,\n\tand its helper functions take their input and output parameters from\n\tconvenient registers, completely ignoring common calling conventions. And\n\tjust to raise the confusion even further, the code doesn't just \u003ci\u003eset\u003c/i\u003e\n\tthese registers for the helper function calls and then restores their\n\toriginal values, but \u003ci\u003epermanently shifts them via additions and\n\tsubtractions\u003c/i\u003e. Unfortunately, these convenient registers also include the\n\t\u003ccode\u003eBP\u003c/code\u003e base pointer to the stack frame of a function… and shifting\n\tthat register throws any intuition behind accessed local variables right out\n\tof the window for a good part of the function, requiring a correctly shifted\n\tview of the stack frame just to make sense of it again.\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e How could such code even have been written?! This\n\tgoes well beyond the already wrong assumption that using more stack space is\n\tsomehow bad, and straight into the territory of self-inflicted pain.\n\u003c/p\u003e\u003cp\u003e\n\tSo while it's not a lot of instructions, it's quite dense and really hard to\n\tfollow. This code would \u003ci\u003ereally\u003c/i\u003e benefit from a decompilation that\n\tanchors all this madness as much as possible in existing C++ structures… so\n\tlet's decompile it anyway? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tDoing so would involve emitting lots of raw machine code bytes to hide the\n\t\u003ccode\u003eSI\u003c/code\u003e and \u003ccode\u003eDI\u003c/code\u003e registers from the compiler, but I\n\talready had a certain\n\t\u003ca href=\"/blog/2020-11-16\"\u003e📝 batshit insane compiler bug workaround abstraction\u003c/a\u003e\n\tlying around that could make such code more readable. Hilariously, it only\n\ttook this one additional use case for that abstraction to reveal itself as\n\tpremature and way too complicated. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Expanding\n\tthe core idea into a full-on x86 instruction generator ended up simplifying\n\tthe code structure a lot. All we really want there is a way to set all\n\tpotential parameters to e.g. a specific form of the \u003ccode\u003eMOV\u003c/code\u003e\n\tinstruction, which can all be expressed as the parameters to a force-inlined\n\t\u003ccode\u003e__emit__()\u003c/code\u003e function. Type safety \u003ci\u003ecan\u003c/i\u003e help by providing\n\toverloads for different operand widths here, but there really is no need for\n\tclasses, templates, or explicit specialization of templates based \u003ci\u003eon\u003c/i\u003e\n\tclasses. We only need a couple of \u003ccode\u003eenum\u003c/code\u003es with opcode, register,\n\tand prefix constants from the x86 reference documentation, and a set of\n\tassociated macros that token-paste pseudoregisters onto the prefixes of\n\tthese \u003ccode\u003eenum\u003c/code\u003e constants.\u003cbr\u003e\n\tAnd that's how you get a custom compile-time assembler in a 1994 C++\n\tcompiler and expand the limits of decompilability even further. What's even\n\ttruly left now? Self-modifying code, layout tricks that can't be replicated\n\twith regularly structured control flow… and that's it. That leaves quite a\n\tfew functions I previously considered undecompilable to be revisited once I\n\tget to work on making this game more portable.\n\u003c/p\u003e\u003cp\u003e\n\tWith that, we've turned the low-level laser code into the expected horrible\n\tmonstrosity that exposes all the hidden complexity in those few ASM\n\tinstructions. The high-level part should be no big deal now… except that\n\twe're immediately bombarded with \u003ccode\u003eFixup overflow\u003c/code\u003e errors at link\n\ttime? Oh well, time to finally learn the true way of fixing this highly\n\tannoying issue in a \u003ci\u003esecond\u003c/i\u003e new piece of decompilation tech – and one\n\tthat might actually be useful for other x86 Real Mode retro developers at\n\tthat.\u003cbr\u003e\n\tEarlier in the RE history of TH04 and TH05, I often wrote about the need to\n\tsplit the two original code segments into multiple segments within two\n\t\u003ci\u003egroups\u003c/i\u003e, which makes it possible to slot in code from different\n\ttranslation units at arbitrary places within the original segment. If we\n\tdon't want to define a unique segment name for each of these slotted-in\n\ttranslation units, we need a way to set custom segment and group names in C\n\tland. Turbo C++ offers two \u003ccode\u003e#pragma\u003c/code\u003es for that:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ccode\u003e#pragma option -zCsegment -zPgroup\u003c/code\u003e – preferred in most\n\tcases as it's equivalent to setting the default segment and group via the\n\tcommand line, but can only be used at the beginning of a translation unit,\n\tbefore the first non-preprocessor and non-comment C language token\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003e#pragma codeseg segment \u0026lt;group\u0026gt;\u003c/code\u003e – necessary if a\n\ttranslation unit needs to emit code into two or more segments\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tFor the most part, these \u003ccode\u003e#pragma\u003c/code\u003es work well, but they seemed to\n\tnot help much when it came to calling \u003ccode\u003enear\u003c/code\u003e functions declared\n\tin different segments within the same group. It took a bit of trial and\n\terror to figure out what was actually going on in that case, but there\n\t\u003ci\u003eis\u003c/i\u003e a clear logic to it:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSymbols are allocated to the segment and group that's active during\n\ttheir first appearance, no matter whether that appearance is a declaration\n\tor definition. Any later appearance of the function in a different segment\n\tis ignored.\u003c/li\u003e\n\t\u003cli\u003eThe linker calculates the 16-bit offsets of such references relative to\n\tthe symbol's \u003ci\u003edeclared\u003c/i\u003e segment, not its actual one. Turbo C++ does\n\t\u003ci\u003enot\u003c/i\u003e show an error or warning if the declared and actual segments are\n\tdifferent, as referencing the same symbol from multiple segments is a valid\n\tuse case. The linker merely throws the \u003ccode\u003eFixup overflow\u003c/code\u003e error if\n\tthe calculated distance exceeds 64 KiB and thus couldn't \u003ci\u003epossibly\u003c/i\u003e fit\n\twithin a \u003ccode\u003enear\u003c/code\u003e reference. With a wrong segment declaration\n\tthough, your code can be incorrect long before a fixup hits that limit.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSummarized in code:\n\u003c/p\u003e\u003cpre\u003e#pragma option -zCfoo_TEXT -zPfoo\n\nvoid bar(void);\nvoid near qux(void); // defined somewhere else, maybe in a different segment\n\n#pragma codeseg baz_TEXT baz\n\n// Despite the segment change in the line above, this function will still be\n// put into `foo_TEXT`, the active segment during the first appearance of the\n// function name.\nvoid bar(void) {\n}\n\n// This function hasn't been declared yet, so it will go into `baz_TEXT` as\n// expected.\nvoid baz(void) {\n\t// This `near` function pointer will be calculated by subtracting the\n\t// flat/linear address of qux() inside the binary from the base address\n\t// of qux()'s declared segment, i.e., `foo_TEXT`.\n\tvoid (near *ptr_to_qux)(void) = qux;\n}\u003c/pre\u003e\u003cp\u003e\n\tSo yeah, you might have to put \u003ccode\u003e#pragma codeseg\u003c/code\u003e into your\n\t\u003ci\u003eheaders\u003c/i\u003e to tell the linker about the correct segment of a\n\t\u003ccode\u003enear\u003c/code\u003e function in advance. 🤯 This is an important insight for\n\teveryone using this compiler, and I'm shocked that none of the Borland C++\n\tbooks documented the interaction of code segment definitions and\n\t\u003ccode\u003enear\u003c/code\u003e references at least at this level of clarity. The TASM\n\tmanuals did have a few pages on the topic of groups, but that syntax\n\tobviously doesn't apply to a C compiler. Fixup overflows in particular are\n\tsuch a common error and really deserved better than the unhelpful \u003cq\u003e🤷\u003c/q\u003e\n\tof an explanation that ended up in the \u003ci\u003eUser's Guide\u003c/i\u003e. Maybe this whole\n\ttechnique of custom code segment names was considered arcane even by 1993,\n\tjudging from the mere three sentences that \u003ccode\u003e#pragma codeseg\u003c/code\u003e was\n\tdocumented with? Still, it must have been common knowledge among Amusement\n\tMakers, because they couldn't have built these exact binaries without\n\tknowing about these details. This is the true solution to\n\t\u003ca href=\"/blog/2021-07-31\"\u003e📝 any issues involving references to \u003ccode\u003enear\u003c/code\u003e functions\u003c/a\u003e,\n\tand I'm glad to see that ZUN did \u003ci\u003enot\u003c/i\u003e in fact lie to the compiler. 👍\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOK, but \u003ci\u003enow\u003c/i\u003e the remaining laser code compiles, and we get to write\n\tC++ code to draw some hitboxes during the two collision-detected states of\n\teach laser. These confirm what the low-level code from earlier already\n\tuncovered: Collision detection against lasers is done by testing a\n\t12×12-pixel box at every 16 pixels along the length of a laser, which leaves\n\tobvious 4-pixel gaps at regular intervals that the player can just pass\n\tthrough. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This adds\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 yet\u003c/a\u003e\n\t\u003ca href=\"/blog/2022-07-10\"\u003e📝 another\u003c/a\u003e\n\t\u003ca href=\"/blog/2022-08-08\"\u003e📝 quirk\u003c/a\u003e to the growing list of quirks that\n\twere either intentional or must have been deliberately left in the game\n\tafter their initial discovery. This is what constants were invented for, and\n\tthere really is no excuse for not using them – \u003ci\u003eespecially\u003c/i\u003e during\n\tintoxicated coding, and/or if you don't have a compile-time abstraction for\n\tQ12.4 literals.\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-01-17-TH05-Sara-laser-gaps.webp?c4dc14b2\" preload=\"none\" controls data-title=\"Without hitboxes\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"844\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-01-17-TH05-Sara-laser-gaps.avi?00bb37cd\"\u003e\u003csource src=\"/blog/static/video/av1/2023-01-17-TH05-Sara-laser-gaps.webm?ee4c79c8\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-01-17-TH05-Sara-laser-gaps.webm?e56388a0\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-01-17-TH05-Sara-laser-gaps.webm?d5bdc461\" type=\"video/webm\"\u003eVideo demonstrating how to pass through TH05's lasers in Sara's final pattern. A second failed attempt a few pixels down demonstrates the size of the gap between collision-detected laser segements.. \u003ca href=\"/blog/static/video/zmbv/2023-01-17-TH05-Sara-laser-gaps.avi?00bb37cd\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-01-17-TH05-Sara-laser-gap-hitboxes.webp?f70a81d1\" preload=\"none\" controls data-title=\"With hitboxes\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"844\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-01-17-TH05-Sara-laser-gap-hitboxes.avi?37f51626\"\u003e\u003csource src=\"/blog/static/video/av1/2023-01-17-TH05-Sara-laser-gap-hitboxes.webm?895beaff\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-01-17-TH05-Sara-laser-gap-hitboxes.webm?598f7068\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-01-17-TH05-Sara-laser-gap-hitboxes.webm?5a7e16dd\" type=\"video/webm\"\u003eVideo demonstrating how to pass through TH05's lasers in Sara's final pattern, showing the hitboxes of each laser as accurately as possible within discrete pixels. \u003ca href=\"/blog/static/video/zmbv/2023-01-17-TH05-Sara-laser-gap-hitboxes.avi?37f51626\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tWhen detecting laser collisions, the game checks the player's single\n\t\tcenter coordinate against any of the aforementioned 12×12-pixel boxes.\n\t\tTherefore, it's correct to split these 12×12 pixels into two 6×6-pixel\n\t\tboxes and assign the other half to the player for a more natural\n\t\tvisualization. Always remember that hitbox visualizations need to keep\n\t\tall colliding entities in mind –\n\t\t\u003ca href=\"/blog/2021-07-31\"\u003e📝 assigning a constant-sized hitbox to \"the player\" and \"the bullets\" will be wrong in most other cases\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tUsing subpixel coordinates in collision detection also introduces a slight\n\tinaccuracy into any hitbox visualization recorded in-engine on a 16-color\n\tPC-98. Since we have to render discrete pixels, we cannot exactly place a\n\tQ12.4 coordinate in the 93.75% of cases where the fractional part is\n\tnon-zero. This is why pretty much every laser segment hitbox in the video\n\tabove shows up as 7×7 rather than 6×6: The actual W×H area of each box is 13\n\tpixels smaller, but since the hitbox lies \u003ci\u003ebetween\u003c/i\u003e these pixels, we\n\tcannot indicate \u003ci\u003ewhere\u003c/i\u003e it lies \u003ci\u003eexactly\u003c/i\u003e, and have to err on the\n\tside of caution. It's also why Reimu's box slightly changes size as she\n\tmoves: Her non-diagonal movement speed is 3.5 pixels per frame, and the\n\tconstant focused movement in the video above halves that to 1.75 pixels,\n\tmaking her end up on an exact pixel every 4 frames. Looking forward to the\n\tglorious future of displays that will allow us to scale up the playfield to\n\t16× its original pixel size, thus rendering the game at its exact internal\n\tresolution of 6144×5888 pixels. Such a port would definitely add a lot of\n\tvalue to the game…\n\u003c/p\u003e\u003cp\u003e\n\tThe remaining high-level laser code is rather unremarkable for the most\n\tpart, but raises one final interesting question: With no explicitly defined\n\tlimit, how wide can a laser be? Looking at the laser structure's 1-byte\n\twidth field and the unsigned comparisons all throughout the update and\n\trendering code, the answer seems to be an obvious 255 pixels. However, the\n\tlaser system also contains an automated shrinking state, which can be most\n\tnotably seen in Mai's wheel pattern. This state shrinks a laser by 2 pixels\n\tevery 2 frames until it reached a width of 0. This presents a problem with\n\todd widths, which would fall below 0 and overflow back to 255 due to the\n\tunsigned nature of this variable. So rather than, I don't know, treating\n\twidth values of 0 as invalid and stopping at a width of 1, or even adding a\n\tcondition for that specific case, the code \u003ci\u003ejust performs a signed\n\tcomparison\u003c/i\u003e, effectively limiting the width of a shrinkable laser to a\n\tmaximum of 127 pixels. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e This small signedness\n\tinconsistency now forces the distinction between shrinkable and\n\tnon-shrinkable lasers onto every single piece of code that uses lasers. Yet\n\tanother instance where\n\t\u003ca href=\"/blog/2022-08-11\"\u003e📝 aiming for a cinematic 30 FPS look\u003c/a\u003e\n\tmade the resulting code much more complicated than if ZUN had just evenly\n\tspread out the subtraction across 2 frames. 🤷\u003cbr\u003e\n\tOh well, it's not as if any of the fixed lasers in the original scripts came\n\tclose to any of these limits. Moving lasers are much more streamlined and\n\tlimited to begin with: Since they're hardcoded to 6 pixels, the game can\n\tsafely assume that they're always thinner than the 28 pixels they get\n\tgradually widened to during their decay animation.\n\u003c/p\u003e\u003cp\u003e\n\tFinally, in case you were missing a mention of hitboxes in the previous\n\tparagraph: Yes, the game always uses the aforementioned 12×12 boxes,\n\tregardless of a laser's width.\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2023-01-17-TH05-Giant-laser.webp?9748b20a\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"304\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2023-01-17-TH05-Giant-laser.avi?f31930c0\"\u003e\u003csource src=\"/blog/static/video/av1/2023-01-17-TH05-Giant-laser.webm?6e7f3c38\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2023-01-17-TH05-Giant-laser.webm?cc8d16de\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2023-01-17-TH05-Giant-laser.webm?523cbc59\" type=\"video/webm\"\u003eVideo of a giant 127-pixel TH05 laser, demonstrating how all lasers use 12×12-pixel hitboxes, regardless of their width. \u003ca href=\"/blog/static/video/zmbv/2023-01-17-TH05-Giant-laser.avi?f31930c0\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThis video also showcases the 127-pixel limit because I wanted\n\tto include the shrink animation for a seamless loop.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat was what, 50% of this blog post just being about complications that\n\tmade laser difficult for no reason? Next up: The first TH01 Anniversary\n\tEdition build, where I finally get to reap the rewards of having a 100%\n\tdecompiled game and write some \u003ci\u003egood\u003c/i\u003e code for once.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-03-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-12-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2023-01-17T17:52:09Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-12-31",
      "url": "https://rec98.nmlgc.net/blog/2022-12-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-01-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-12-31\"\u003e\u003ctime datetime=\"2022-12-31T23:57:00Z\"\u003e2022-12-31 23:57\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0226\"\u003eP0226\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Running at full speed on modern systems / Basic locale independence)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/M0002...P0226\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e, alp-bib\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cblockquote\u003e\u0026gt; \"OK, TH03/TH04/TH05 cutscenes done, let's quickly finish the \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e MediaWiki upgrade. Just some scripting and verification left, it will be done so quickly that I don't even have to mention it on this blog\"\n\u0026gt; Still not done after 3 weeks\n\u0026gt; Blocked by \u003ca href=\"https://gerrit.wikimedia.org/r/c/mediawiki/extensions/Translate/+/871904\"\u003eone final critical bug that really should be fixed upstream\u003c/a\u003e\n\u0026gt; Code reviewers are probably on vacation\u003c/blockquote\u003e\u003cp\u003e\n\tAnd so, the year unfortunately ended with yet another slow month. During the\n\tMediaWiki upgrade, I was slowly decompiling the TH05 Sara fight on the side,\n\tbut stumbled over one interesting but high-maintenance detail there that\n\twould really enhance her blog post. TH02 would need a lot of attention for\n\tthe basic rendering calls as well…\n\u003c/p\u003e\u003cp\u003e\n\t…so let's end the year with Shuusou Gyoku instead, looking at its most\n\tcritical issue in particular. As if that were the easy option here…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#32bit-2022-12-31\"\u003eThe simple and practical fix for all of Shuusou Gyoku's rendering issues\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#unicode-2022-12-31\"\u003eBasic locale-independent file I/O\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr\u003e\u003cp id=\"32bit-2022-12-31\"\u003e\n\tThe game does not run properly on modern Windows systems due to its usage of\n\tthe ancient DirectDraw APIs, with issues ranging from unbearable slowdown to\n\tglitched colors to the game not even starting at all. Thankfully, Shuusou\n\tGyoku is not the only ancient Windows game affected by these issues, and\n\tpeople have developed a variety of generic DirectDraw wrappers and patches\n\tfor playing such games on modern systems. Out of all these, \u003ca\n\thref=\"https://github.com/narzoul/DDrawCompat\"\u003eDDrawCompat\u003c/a\u003e is one of the\n\tsimpler solutions for Shuusou Gyoku in particular: Just drop its\n\t\u003ccode\u003eddraw\u003c/code\u003e proxy DLL into the game directory, and the game will run\n\tas it's supposed to.\u003cbr\u003e\n\tSo let's just bundle that DLL with all my future Shuusou Gyoku releases\n\tthen? That \u003ci\u003ewould\u003c/i\u003e have been the quick and dirty option, coming with\n\tseveral drawbacks:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eLinux users might be annoyed by the potential need to configure a native\n\tDLL override for \u003ccode\u003eddraw.dll\u003c/code\u003e. It's not too much of an issue as we\n\tcould simply rename the DLL and replace the import with the new name.\n\tHowever, doing that reproducibly would already involve changes to either the\n\tDDrawCompat or Shuusou Gyoku build process.\u003c/li\u003e\n\t\u003cli\u003eWin32 API hooking is another potential point of failure in general,\n\trequiring continual maintenance for new Windows versions. This is not even a\n\thypothetical concern: DDrawCompat does rely on particularly volatile Win32\n\tAPI details, to the point that the recent Windows 11 22H2 update \u003ca\n\thref=\"https://github.com/narzoul/DDrawCompat/commit/f1d8dbd1cb96fb641c31d55c9cf576406a5c2d01\"\u003ecompletely\n\tbroke it, causing a hang at startup that required a workaround\u003c/a\u003e.\u003cbr\u003e\n\tBut sure, it's still just a single third-party component. Keeping it up to\n\tdate doesn't sound too bad by itself…\u003c/li\u003e\n\t\u003cli\u003e…if DDrawCompat weren't evolving way beyond what we need to keep Shuusou\n\tGyoku running. Being a typical DirectDraw wrapper, it has always aimed to\n\tsolve all sorts of issues in old DirectDraw games. However, the latest\n\tversion, 0.4.0, has gone above and beyond in this regard, adding \u003ca\n\thref=\"https://github.com/narzoul/DDrawCompat/wiki/Configuration\"\u003elots of\n\tconfiguration options\u003c/a\u003e with default settings that \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/3#issuecomment-1264267695\"\u003eactually\n\tbreak Shuusou Gyoku\u003c/a\u003e.\u003cbr\u003e\n\tTo get a glimpse of how this is likely to play out, we only have to look at\n\tthe more mature \u003ca href=\"https://sourceforge.net/projects/dxwnd/\"\u003eDxWnd\u003c/a\u003e\n\tproject. In its expert mode, DxWnd features three rows of tabs, each packed\n\twith checkboxes that toggle individual hacks, and \u003ci\u003emost\u003c/i\u003e of these are\n\trelated to \u003ci\u003esomething\u003c/i\u003e that Shuusou Gyoku could be affected by. Imagine\n\tchecking a precise permutation of a three-digit number of checkboxes just to\n\tkeep an old game running at full speed on modern systems…\u003c/li\u003e\n\t\u003cli\u003eFinally, aesthetic and bloat considerations. If\n\t\u003ca href=\"/blog/2022-09-04\"\u003e📝 C++ fstreams\u003c/a\u003e were already too embarrassing\n\twith the ~100\u0026nbsp;KB of bloat they add to the binary, a 565\u0026nbsp;KiB DLL is\n\teven worse. And that's the \u003ci\u003eold\u003c/i\u003e version 0.3.2 – version 0.4.0 comes in\n\tat 2.43\u0026nbsp;\u003ci\u003eMiB\u003c/i\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tFortunately, I had the budget to dig a bit deeper and figure out what\n\t\u003ci\u003eexactly\u003c/i\u003e DDrawCompat does to make Shuusou Gyoku work properly. Turns\n\tout that among all the hooks and patches, the game only needs the most\n\tcentral one: Enforcing a 32-bit display mode regardless of whatever lower\n\tbit depth the game requests natively, combined with converting the game's\n\tpixel buffer to 32-bit on the fly.\u003cbr\u003e\n\tSo does this mean that adding 32-bit to the game's list of supported bit\n\tdepths is everything we have to do?\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\u003cimg\n\tsrc=\"/blog/static/2022-12-31-SH01-32-bit.png?033be8f1\"\n\talt=\"The new 32-bit rendering option in the Shuusou Gyoku P0226 build.\"\n\u003e\u003cfigcaption\u003e\n\tInterestingly, Shuusou Gyoku already saved the DirectDraw enumeration flag\n\tthat indicates support for 32-bit display modes. The official version just\n\tdid nothing with it.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tWell, \u003ci\u003ealmost\u003c/i\u003e everything. Initially, this surprised me as well: With\n\tall the \u003ccode\u003eif\u003c/code\u003e statements checking for precise bit depths, you\n\twould think that supporting one more bit depth would be way harder in this\n\tcode base. As it turned out though, these conditional branches are not\n\t\u003ci\u003ereally\u003c/i\u003e about 8-bit or 16-bit color for the most part, but instead\n\tdifferentiate between two very distinct rendering approaches:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\"8-bit\" is a pure 2D mode with palettized colors,\u003c/li\u003e\n\t\u003cli\u003ewhile \"16-bit\" is a hybrid 2D/3D mode that uses Direct3D \u003cspan\n\tclass=\"hovertext\" title=\"(sic)\"\u003e2\u003c/span\u003e on top of DirectDraw, with\n\t3-channel RGB colors.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tConsequently, most of these branches deal with differences between these two\n\tapproaches that couldn't be nicely abstracted away in pbg's renderer\n\tinterface: Specific palette changes that are exclusive to \"8-bit\" mode, or\n\tcertain entities and effects whose Direct3D draw calls in \"16-bit\" mode\n\trequire tailor-made approximations for the \"8-bit\" mode. Since our new\n\t32-bit mode is equivalent to the 16-bit mode in all of these branches, I\n\tonly needed to replace the raw number comparisons with more meaningful\n\tmethod calls.\n\u003c/p\u003e\u003cp\u003e\n\tThat only left a very small number of 2D raster effects that directly write\n\tto or read from DirectDraw surface memory, and therefore do need to know the\n\tbit size of each pixel. Thanks to \u003ccode\u003estd::variant\u003c/code\u003e and\n\t\u003ccode\u003estd::visit()\u003c/code\u003e, adding 32-bit support becomes trivial here: By\n\trewriting the code in a generic manner that derives all offsets from the\n\ttemplate type, you only have to say \u003cq\u003e\u003ca\n\thref=\"https://github.com/nmlgc/ssg/commit/20e4544efcdcd01fd70c74fd312c2bc86821ee2f\"\u003ehey,\n\tI'd like to have 32-bit as well\u003c/a\u003e\u003c/q\u003e, and C++ will automatically\n\tinstantiate correct 32-bit variants of all bit depth-dependent code\n\tsnippets.\u003cbr\u003e\n\tThere are only three features in the entire game that access pixel buffers\n\tthis way: a color key retrieval function, the lens ball animation on the\n\tlogo screen, and… the ending staff roll? Sure, the text sprites fade in and\n\tout, but so does the picture next to it, using Direct3D alpha blending or\n\tpalette color ramping depending on the current rendering mode. Instead, the\n\tonly reason why these sprites directly access their pixel buffer is… an\n\tunused and pretty wild spiral effect. 😮 It's still part of the code, and\n\tonly doesn't show up because \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/a7d4ccd2e2c4f38db68804f010ac4d6b930d51a7/GIAN07/ENDING.CPP#L314-L319\"\u003ethe\n\tparameters that control its timing were commented out before release\u003c/a\u003e:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-12-31-SH01-Ending-spiral.webp?43b20b82\" preload=\"none\" controls loop width=\"640\" height=\"480\" data-fps=\"60\" data-frame-count=\"2874\" style=\"aspect-ratio: 640 / 480\" data-lossless=\"/blog/static/video/zmbv/2022-12-31-SH01-Ending-spiral.avi?aa757378\"\u003e\u003csource src=\"/blog/static/video/av1/2022-12-31-SH01-Ending-spiral.webm?696555cb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-12-31-SH01-Ending-spiral.webm?9077ba31\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-12-31-SH01-Ending-spiral.webm?88aeaced\" type=\"video/webm\"\u003eTODO. \u003ca href=\"/blog/static/video/zmbv/2022-12-31-SH01-Ending-spiral.avi?aa757378\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003cfigcaption\u003e\n\tThey probably considered it \u003ci\u003etoo \u003c/i\u003e wild for the \u003ca\n\thref=\"https://www.youtube.com/watch?v=SRbwQahZBsE\"\u003emood\u003c/a\u003e of this\n\tending.\u003cbr\u003e\n\tThe main ending text was the only remaining issue of mojibake present in my\n\tprevious Shuusou Gyoku builds, and is now fixed as well. Windows \u003ci\u003ecan\u003c/i\u003e\n\trender Shift-JIS text via GDI even outside Japanese locale, but only when\n\texplicitly selecting a font that supports the \u003ccode\u003eSHIFTJIS_CHARSET\u003c/code\u003e,\n\tand the game simply didn't select \u003ci\u003eany\u003c/i\u003e font for rendering this text.\n\tThus, GDI fell back onto its default font, which obviously is only\n\tguaranteed to support the \u003ccode\u003eSHIFTJIS_CHARSET\u003c/code\u003e if your system\n\tlocale is set to Japanese. This is why the font in the original game might\n\t\u003ca href=\"https://youtu.be/IWOhouJ7c04?t=1241\"\u003elook\u003c/a\u003e \u003ca\n\thref=\"https://youtu.be/lRH-fhSaDSE?t=361s\"\u003edifferent\u003c/a\u003e between systems.\n\tFor my build, I chose the font that would appear on a clean Windows\n\tinstallation – a basic 400-weighted MS Gothic at font size 16, which is\n\talready used all throughout the game.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tAlright, 32-bit mode complete, let's set it as the default if possible… and\n\tbreak compatibility to the original \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e format in the\n\tprocess? When validating this file, the original game only allows the\n\toriginally supported 8-bit or 16-bit modes. Setting the\n\t\u003ccode\u003eBitDepth\u003c/code\u003e field to any other value causes the \u003ci\u003eentire\u003c/i\u003e file\n\tto be reset to its defaults, re-locking the Extra Stage in the process.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tIntroducing a \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/34\"\u003ebackward-compatible version\n\tsystem for \u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e\u003c/a\u003e was beyond the scope of this push.\n\tChanging the validation to a per-field approach was a good small first step\n\tto take though. The new build no longer validates the \u003ccode\u003eBitDepth\u003c/code\u003e\n\tfield against a fixed list, but against the actually supported bit depths on\n\tyour system, picking a different supported one if necessary. With the\n\toriginal approach, this would have caused your entire configuration to fail\n\tthe validation check. Instead, you can now safely update to the new build\n\twithout losing your option settings, or your previously unlocked access to\n\tthe Extra Stage.\u003cbr\u003e\n\tSide note: The validation limit for starting bombs is off by one, and the\n\tone for starting lives check is off by two. By modifying\n\t\u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e, you could theoretically get new games to start with\n\t7 lives and 3 bombs… if you then calculate a correct checksum for your\n\thacked config file, that is. 🧑‍💻\n\u003c/p\u003e\u003cp\u003e\n\tInterestingly, DirectDraw doesn't even indicate support for 8-bit or 16-bit\n\tcolor on systems that are affected by the initially mentioned issues.\n\tTherefore, these issues are \u003ci\u003enot\u003c/i\u003e the fault of DirectDraw, but of\n\tShuusou Gyoku, as the original release requested a bit depth \u003ci\u003ethat it has\n\teven verified to be unsupported\u003c/i\u003e. Unfortunately, Windows sides with\n\t\u003cs\u003eSim City\u003c/s\u003e Shuusou Gyoku here: If you previously experimented with the\n\tWindows app compatibility settings, you might have ended up with the\n\t\u003ccode\u003eDWM8And16BitMitigation\u003c/code\u003e flag assigned to the full file path of\n\tyour Shuusou Gyoku executable in either\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ccode\u003eHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\u003c/code\u003e, or\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eHKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs the term \u003ci\u003emitigation\u003c/i\u003e suggests, these modes are (poorly) emulated,\n\twhich is exactly what causes the issues with this game in the first place.\n\tSure, this might be the lesser evil from the point of view of an operating\n\tsystem: If you don't have the budget for a full-blown DDrawCompat-style\n\tDirectDraw wrapper, you might consider it better for users to have the game\n\trun poorly than have it fail at startup due to incorrect API usage.\n\tControlling this with a flag that sticks around for future runs of a binary\n\tis definitely suboptimal though, \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues/33\"\u003eespecially given how hard it\n\tis to programmatically remove this flag within the binary itself\u003c/a\u003e. It\n\tonly adds additional complexity to the ideal clean upgrade path.\u003cbr\u003e\n\tSo, make sure to check your registry and manually remove these flags for the\n\ttime being. Without them, the new \u003ci\u003eConfig → Graphic\u003c/i\u003e menu will\n\tcorrectly prevent you from selecting anything else but 32-bit on modern\n\tWindows.\n\u003c/p\u003e\u003chr\u003e\u003cp id=\"unicode-2022-12-31\"\u003e\n\tAfter all that, there was just enough time left in this push to implement\n\tbasic locale independence, as requested by the \u003ci\u003eSeihou development\u003c/i\u003e\n\tDiscord group, without looking into automatic fixes for previous mojibake\n\tfilenames yet. Combining \u003ccode\u003estd::filesystem::path\u003c/code\u003e with the native\n\tWin32 API should be straightforward and bloat-free, especially with all the\n\tabstractions I've been building, right?\u003cbr\u003e\n\tWell, turns out that \u003ccode\u003estd::filesystem::path\u003c/code\u003e does not\n\t\u003ci\u003eactually\u003c/i\u003e meet my expectations. At least as long as it's not\n\t\u003ccode\u003econstexpr\u003c/code\u003e-enabled, because you \u003ci\u003estill\u003c/i\u003e get the unfortunate\n\tconversion from narrow to wide encoding at runtime, even for globals with\n\tstatic storage duration. That brings us back to writing our path abstraction\n\tin terms of the regular \u003ccode\u003estd::string\u003c/code\u003e and\n\t\u003ccode\u003estd::wstring\u003c/code\u003e containers, which at least allow us to enforce the\n\trespective encoding at compile time. Even \u003ccode\u003estd::string_view\u003c/code\u003e only\n\tadds to the complexity here, as its strings are never inherently\n\tnull-terminated, which is required by both the POSIX and Win32 APIs. Not to\n\tmention dynamic filenames: C++20's \u003ccode\u003estd::format()\u003c/code\u003e would be the\n\tobvious idiomatic choice here, but using it almost \u003ci\u003edoubles\u003c/i\u003e the size\n\tof the compiled binary… 🤮\u003cbr\u003e\n\tIn the end, the most bloat-free way of implementing C++ file I/O in 2023 is\n\tstill the same as it was 30 years ago: Call system APIs, roll a custom\n\tabstraction that conditionally uses the \u003ccode\u003eL\u003c/code\u003e prefix, and pass\n\taround raw pointers. And if you need a dynamic filename, just write the\n\tdynamic characters into arrays at fixed positions. Just as PC-98 Touhou used\n\tto do… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tOh, and the game's window also uses a Unicode title bar now.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's it for this push! Make sure to rename your configuration\n\t(\u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e), score (\u003ccode\u003e秋霜SC.DAT\u003c/code\u003e), and replay\n\t(\u003ccode\u003e秋霜りぷ*.DAT\u003c/code\u003e) filenames if you were previously running the\n\tgame on a non-Japanese locale, and then grab the new build:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0226\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0226\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tWith that, we've got the most critical bugs out of the way, but the \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues?q=is%3Aopen+is%3Aissue\"\u003enumber of\n\tpotential fixes and features in Shuusou Gyoku\u003c/a\u003e has only increased.\n\tLooking forward to what's next in this apparent \u003ca\n\thref=\"https://twitter.com/WishMakers_TH/status/1608567030193750016\"\u003eSeihou\n\trevolution\u003c/a\u003e, later in 2023!\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Starting the new year with all my plans hopefully working out for\n\tonce. TH05 Sara very soon, ZMBV code review afterward, low-hanging fruit of\n\tthe TH01 Anniversary Edition after that, and then kicking off TH02 with a\n\tbunch of low-level blitting code.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2023-01-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-12-31T23:57:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-11-30",
      "url": "https://rec98.nmlgc.net/blog/2022-11-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-12-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-10-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-11-30\"\u003e\u003ctime datetime=\"2022-11-30T22:20:03Z\"\u003e2022-11-30 22:20\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0223\"\u003eP0223\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (Cutscenes, part 1/3: State + Box and picture rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/139746c...371292d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0224\"\u003eP0224\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (Cutscenes, part 2/3: Script commpands)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/371292d...8118e61\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0225\"\u003eP0225\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (Cutscenes, part 3/3: Interpreter loop + TH05 rendering boilerplate, part 1/?)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8118e61...4f85326\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003erosenrose, Blue Bolt, \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, Yanga, Enderwolf, \u003ca class=\"customer\" href=\"https://www.youtube.com/@32th\"\u003e32th System\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t._2022-11-30 tr:not(:first-child) {\n\t\tborder-top: var(--table-border);\n\t\tborder-top-color: var(--c-lightgray);\n\t}\n\t#cutscene-2022-11-30 tbody td:nth-child(1),\n\t#cutscene-2022-11-30 tbody td:nth-child(2),\n\t#cutscene-2022-11-30 tbody td:nth-child(3) {\n\t\tmin-width: var(--icon-width);\n\t}\n\t#cutscene-2022-11-30 tbody td:nth-child(1) {\n\t\tpadding-right: 0;\n\t}\n\t#cutscene-2022-11-30 tbody td:nth-child(2) {\n\t\tpadding-left: 0;\n\t\tpadding-right: 0;\n\t}\n\t#cutscene-2022-11-30 tbody td:nth-child(3) {\n\t\tpadding-left: 0;\n\t}\n\u003c/style\u003e\n\u003cp\u003e\n\tMore than three months without any reverse-engineering progress! It's been\n\tway too long. Coincidentally, we're at least back with a surprising 1.25% of\n\toverall RE, achieved within just 3 pushes. The ending script system is not\n\tonly more or less the same in TH04 and TH05, but actually originated in\n\tTH03, where it's also used for the cutscenes before stages 8 and 9. This\n\tmeans that it was one of the final pieces of code shared between three of\n\tthe four remaining games, which I got to decompile at roughly 3× the usual\n\tspeed, or ⅓ of the price.\u003cbr\u003e\n\tThe only other bargains of this nature remain in \u003ccode\u003eOP.EXE\u003c/code\u003e. The\n\tMusic Room is largely equivalent in all three remaining games as well, and\n\tthe sound device selection, ZUN Soft logo screens, and main/option menus are\n\tthe same in TH04 and TH05. A lot of that code is in the \"technically RE'd\n\tbut not yet decompiled\" ASM form though, so it would shift Finalized% more\n\tsignificantly than RE%. Therefore, make sure to order the new\n\t\u003ci\u003eFinalization\u003c/i\u003e option rather than \u003ci\u003eReverse-engineering\u003c/i\u003e if you\n\twant to make number go up.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#overview-2022-11-30\"\u003eGeneral overview\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#games-2022-11-30\"\u003eGame-specific differences\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#ref-2022-11-30\"\u003eCommand reference\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#translations-2022-11-30\"\u003eThoughts about translation support\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"overview-2022-11-30\"\u003e\u003cp\u003e\n\tSo, cutscenes. On the surface, the .TXT files look simple enough: You\n\tdirectly write the text that should appear on the screen into the file\n\twithout any special markup, and add commands to define visuals, music, and\n\tother effects at any place within the script. Let's start with the basics of\n\thow text is rendered, which are the same in all three games:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFirst off, the text area has a size of 480×64 pixels. This means that it\n\tdoes \u003ci\u003enot\u003c/i\u003e correspond to the tiled area painted into TH05's\n\t\u003ccode\u003eEDBK?.PI\u003c/code\u003e images:\u003cfigure\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-Cutscene-text-area.png?412d2b94\"\n\t\talt=\"The text area of the TH03/TH04/TH05 cutscene system.\"\n\t\u003e\u003cfigcaption\u003e\n\t\tThe yellow area is designated for character names.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eSince the font weight can be customized, all text is rendered to VRAM.\n\tThis also includes gaiji, despite them ignoring the font weight\n\tsetting.\u003c/li\u003e\n\t\u003cli\u003eThe system supports automatic line breaks on a per-glyph basis, which\n\tmove the text cursor to the beginning of the \u003cspan style=\"color:\n\tred\"\u003ered\u003c/span\u003e text area. This might seem like a piece of long-forgotten\n\tancient wisdom at first, considering the absence of automatic line breaks in\n\tWindows Touhou. However, ZUN probably implemented it more out of pure\n\tnecessity: Text in VRAM needs to be unblitted when starting a new box, which\n\tis way more straightforward and performant if you only need to worry\n\tabout a fixed area.\u003c/li\u003e\n\t\u003cli\u003eThe system also automatically starts a new (key press-separated) text\n\tbox after the end of the 4\u003csup\u003eth\u003c/sup\u003e line. However, the text cursor is\n\talso unconditionally moved to the top-left corner of the yellow \u003ci\u003ename\u003c/i\u003e\n\tarea when this happens, which is almost certainly not what you expect, given\n\tthat automatic line breaks stay within the red area. A script author might\n\tas well add the necessary text box change commands manually, if you're\n\tforced to anticipate the automatic ones anyway…\u003cbr\u003e\n\tDue to ZUN forgetting an unblitting call during the TH05 refactoring of the\n\tbox background buffer, this feature is even completely broken in that game,\n\tas any new text will simply be blitted on top of the old one:\u003cfigure\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-TH05-Cutscene-box-feed-bug.png?ffcd5411\"\n\t\talt=\"The effects of relying on automatically added text boxes in TH05's cutscene system.\"\n\t\u003e\u003cfigcaption\u003e\n\t\tWait, why are we already talking about game-specific differences after\n\t\tall? Also, note how the ⏎ animation appears one line below where you'd\n\t\texpect it.\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\u003cli\u003eOverall, the system is geared toward exclusively full-width text. As\n\texemplified by the 2014 static English patches and the screenshots in this\n\tblog post, half-width text is \u003ci\u003epossible\u003c/i\u003e, but comes with a lot of\n\tasterisks attached:\u003cul\u003e\n\t\t\u003cli\u003eEach loop of the script interpreter starts by looking at the next\n\t\tbyte to distinguish commands from text. However, this step also skips\n\t\tover every ASCII space and control character, i.e., every byte\n\t\t≤\u0026nbsp;32. If you only intend to display full-width glyphs anyway, this\n\t\tsort of makes sense: You gain complete freedom when it comes to the\n\t\tphysical layout of these script files, and it especially allows commands\n\t\tto be freely separated with spaces and line breaks for improved\n\t\treadability. Still, enforcing commands to be separated exclusively by\n\t\tline breaks might have been even better for readability, and would have\n\t\tfreed up ASCII spaces for regular text…\u003c/li\u003e\n\t\t\u003cli\u003eNon-command text is blindly processed and rendered two bytes at a\n\t\ttime. The rendering function interprets these bytes as a Shift-JIS\n\t\tstring, so you \u003ci\u003ecan\u003c/i\u003e use half-width characters here. While the\n\t\tsecond byte can even be an ASCII \u003ccode\u003e0x20\u003c/code\u003e space due to the\n\t\tparser's blindness, all half-width characters must still occur in pairs\n\t\tthat can't be interrupted by commands:\u003cfigure\u003e\u003cimg\n\t\t\tsrc=\"/blog/static/2022-11-30-Cutscene-halfwidth.png?804a30fc\"\n\t\t\talt=\"Issues with half-width text in the TH03/TH04/TH05 cutscene system.\"\n\t\t\t\u003e\u003c/figure\u003e\u003c/li\u003e\n\t\t\u003cli\u003eAs a workaround for at least the ASCII space issue, you can replace\n\t\tthem with any of the \u003ca\n\t\thref=\"https://en.wikipedia.org/w/index.php?title=Shift_JIS\u0026oldid=1118113512#Shift_JIS_byte_map\"\u003eunassigned\n\t\tShift-JIS lead bytes\u003c/a\u003e – \u003ccode\u003e0x80\u003c/code\u003e, \u003ccode\u003e0xA0\u003c/code\u003e, or\n\t\tanything between \u003ccode\u003e0xF0\u003c/code\u003e and \u003ccode\u003e0xFF\u003c/code\u003e inclusive.\n\t\tThat's what you see in all screenshots of this post that display\n\t\thalf-width spaces.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eFinally, did you know that you can hold \u003ckbd\u003eESC\u003c/kbd\u003e to fast-forward\n\tthrough these cutscenes, which skips most frame delays and reduces the rest?\n\tDue to the blocking nature of all commands, the \u003ckbd\u003eESC\u003c/kbd\u003e key state is\n\tonly updated between commands or 2-byte text groups though, so it can't\n\tinterrupt an ongoing delay.\u003c/li\u003e\n\u003c/ul\u003e\u003chr id=\"games-2022-11-30\"\u003e\u003cp\u003e\n\tSuperficially, the list of game-specific differences doesn't look too long,\n\tand can be summarized in a rather short table:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable class=\"comparison\"\u003e\u003cthead\u003e\u003ctr\u003e\n\t\u003cth\u003e\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e TH03\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e TH04\u003c/th\u003e\n\t\u003cth\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e TH05\u003c/th\u003e\n\u003c/tr\u003e\u003c/thead\u003e\u003ctbody\u003e\u003ctr\u003e\n\t\u003cth\u003eScript size limit\u003c/th\u003e\n\t\u003ctd\u003e65536 bytes (heap-allocated)\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003e8192 bytes (statically allocated)\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eDelay between every 2 bytes of text\u003c/th\u003e\n\t\u003ctd\u003e1 frame by default, customizable via \u003ca\n\thref=\"#2022-11-30-v-th03\"\u003e\u003ccode\u003e\\v\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003eNone\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eText delay when holding \u003ckbd\u003eESC\u003c/kbd\u003e\u003c/th\u003e\n\t\u003ctd\u003eVarying speed-up factor\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003eNone\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eVisibility of new text\u003c/th\u003e\n\t\u003ctd\u003eImmediately typed onto the screen\u003c/td\u003e\n\t\u003ctd colspan=\"2\"\u003eRendered onto invisible VRAM page, faded in on wait\n\tcommands\u003c/td\u003e\n\u003c/tr\u003e\u003ctr id=\"2022-11-30-oldtext\"\u003e\n\t\u003cth\u003eVisibility of old text\u003c/th\u003e\n\t\u003ctd colspan=\"2\"\u003eUnblitted when starting a new box\u003c/td\u003e\n\t\u003ctd\u003eLeft on screen until crossfaded out with new text\u003c/td\u003e\n\u003c/tr\u003e\u003ctr id=\"2022-11-30-advance\"\u003e\n\t\u003cth\u003eKey binding for advancing the script\u003c/th\u003e\n\t\u003ctd colspan=\"2\"\u003eAny key\u003c/td\u003e\n\t\u003ctd\u003e\u003ckbd\u003e⏎ Return\u003c/kbd\u003e, \u003ckbd\u003eShot\u003c/kbd\u003e, or \u003ckbd\u003eESC\u003c/kbd\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eAnimation while waiting for an advance key\u003c/th\u003e\n\t\u003ctd colspan=\"2\"\u003eNone\u003c/td\u003e\n\t\u003ctd\u003e\u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDwABACwAAAAAEAAQAAACK4yPicDtCdQ6kT5T62q4Kx5kEihSGONpaLiWziW+cCqboTXaNwTJoAQ0FAAAIfkECQ8AAQAsAQABAA4ADgAAAiNMgKl2DOwanAZRai/MdXWcfOBzkFNYbh3JZairvGUcmrRXAAAh+QQJDwABACwBAAEADgAOAAACI0yAqXbqB9sziFVmAdaY8t5d4PaNHplk4NKMabte2seltMMWACH5BAUPAAEALAEAAQAOAA4AAAIlTICpdszZgJMHGXsprFlh6y1c5T3U6KBNqrKZm2KdLJ3zSoZUAQA7\"\n\t\talt=\"⏎⃣\"\n\t\u003e, past right edge of current row\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eInexplicable delays\u003c/th\u003e\n\t\u003ctd colspan=\"2\"\u003eNone\u003c/td\u003e\n\t\u003ctd\u003e1 frame before changing pictures and after rendering new text boxes\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eAdditional delay per interpreter loop\u003c/th\u003e\n\t\u003ctd\u003e614.4\u0026nbsp;µs\u003c/td\u003e\n\t\u003ctd\u003eNone\u003c/td\u003e\n\t\u003ctd\u003e614.4\u0026nbsp;µs\u003c/td\u003e\n\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\u003cfigcaption\u003e\n\tThe 614.4\u0026nbsp;µs correspond to the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/commit/8dfc2cd\"\u003enecessary delay for\n\tworking around the repeated \u003ci\u003ekey up\u003c/i\u003e and \u003ci\u003ekey down\u003c/i\u003e events sent by\n\tPC-98 keyboards when holding down a key\u003c/a\u003e. While the absence of this delay\n\tsignificantly speeds up TH04's interpreter, it's also the reason why that\n\tgame will stop recognizing a held \u003ckbd\u003eESC\u003c/kbd\u003e key after a few seconds,\n\trequiring you to press it again.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tIt's when you get into the implementation that the combined three systems\n\treveal themselves as a giant mess, with more like 56 differences between the\n\tgames. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Every single new weird line of code opened up\n\tanother can of worms, which ultimately made all of this end up with 24\n\tpieces of bloat and 14 bugs. The worst of these should be quite interesting\n\tfor the general PC-98 homebrew developers among my audience:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe final official 0.23 release of master.lib has a bug in\n\t\u003ccode\u003egraph_gaiji_put*()\u003c/code\u003e. To calculate the JIS X 0208 code point for\n\ta gaiji, it is enough to \u003ccode\u003eADD 5680h\u003c/code\u003e onto the gaiji ID. However,\n\tthese functions accidentally use \u003ccode\u003eADC\u003c/code\u003e instead, which incorrectly\n\tadds the x86 carry flag on top, causing weird off-by-one errors based on the\n\tprevious program state. ZUN did fix this bug directly inside master.lib for\n\tTH04 and TH05, but still needed to work around it in TH03 by subtracting 1\n\tfrom the intended gaiji ID. Anyone up for maintaining a bug-fixed master.lib\n\trepository?\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003eThe worst piece of bloat comes from TH03 and TH04 needlessly\n\tswitching the visibility of VRAM pages while blitting a new 320×200 picture.\n\tThis makes it much harder to understand the code, as the mere existence of\n\tthese page switches is enough to suggest a more complex interplay between\n\tthe two VRAM pages which doesn't actually exist. Outside this visibility\n\tswitch, page 0 is always supposed to be shown, and page 1 is always used\n\tfor temporarily storing pixels that are later crossfaded onto page 0. This\n\tis also the only reason why TH03 has to render text and gaiji onto both VRAM\n\tpages to begin with… and because TH04 doesn't, changing the picture in the\n\tmiddle of a string of text is technically bugged in that game, even though\n\tyou only get to temporarily see the new text on very underclocked PC-98\n\tsystems.\u003c/p\u003e\u003cp\u003e\n\tThese performance implications made me wonder why cutscenes even bother with\n\twriting to the second VRAM page anyway, before copying each crossfade step\n\tto the visible one.\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 We learned in June how costly EGC-\"accelerated\" inter-page copies are\u003c/a\u003e;\n\tshouldn't it be faster to just blit the image once rather than twice?\u003cbr\u003e\n\tWell, master.lib decodes .PI images into a packed-pixel format, and\n\tunpacking such a representation into bitplanes on the fly is just about the\n\tworst way of blitting you could possibly imagine on a PC-98. EGC inter-page\n\tcopies are already fairly disappointing at 42 cycles for every 16 pixels, if\n\twe look at the i486 and ignore VRAM latencies. But under the same\n\tconditions, packed-pixel unpacking comes in at 81 cycles for every \u003ci\u003e8\u003c/i\u003e\n\tpixels, or almost 4× slower. On lower-end systems, that can easily sum up to\n\tmore than one frame for a 320×200 image. While I'd argue that the resulting\n\ttearing could have been an acceptable part of the transition between two\n\timages, it's understandable why you'd want to avoid it in favor of the\n\tpure effect on a slower framerate.\u003cbr\u003e\n\tReally makes me wonder why master.lib didn't just directly decode .PI images\n\tinto bitplanes. The performance impact on load times should have been\n\tnegligible? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e \u003ca\n\thref=\"https://mooncore.eu/bunny/txt/pi-pic.htm\"\u003eIt's such a good format for\n\tthe often dithered 16-color artwork you typically see on PC-98\u003c/a\u003e, and\n\tdeserves better than master.lib's implementation which is both slow to\n\tdecode and slow to blit.\n\u003c/p\u003e\u003c/li\u003e\u003c/ul\u003e\u003chr id=\"ref-2022-11-30\"\u003e\u003cp\u003e\n\tThat brings us to the individual script commands… and yes, I'm going to\n\tdocument every single one of them. Some of their interactions and edge cases\n\tare not clear at all from just looking at the code.\n\u003c/p\u003e\u003cp\u003e\n\tAlmost all commands are preceded by… well, a \u003ccode\u003e0x5C\u003c/code\u003e lead byte.\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e Which raises the question of whether we should\n\tdocument it as an ASCII-encoded \\\u0026nbsp;backslash, or a Shift-JIS-encoded\n\t¥\u0026nbsp;yen sign. From a gaijin perspective, it seems obvious that it's a\n\tbackslash, as it's consistently displayed as one in most of the editors you\n\twould actually use nowadays. But interestingly, \u003ccode\u003eiconv\n\t-f\u0026nbsp;shift-jis -t\u0026nbsp;utf-8\u003c/code\u003e does convert any \u003ccode\u003e0x5C\u003c/code\u003e\n\tlead bytes to actual \u003ccode\u003e¥ U+00A5 YEN SIGN\u003c/code\u003e code points\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e.\u003cbr\u003e\n\tUltimately, the distinction comes down to the font. There \u003ci\u003eare\u003c/i\u003e fonts\n\tthat still render \u003ccode\u003e0x5C\u003c/code\u003e as \u003ccode\u003e¥\u003c/code\u003e, but mainly do so out\n\tof an obvious concern about backward compatibility to JIS X 0201, where this\n\tmapping originated. Unsurprisingly, this group includes MS Gothic/Mincho,\n\tthe old Japanese fonts from Windows 3.1, but even Meiryo and Yu\n\tGothic/Mincho, Microsoft's modern Japanese fonts. Meanwhile, pretty much\n\tevery other modern font, and freely licensed ones in particular, render this\n\tcode point as \u003ccode\u003e\\\u003c/code\u003e, even if you set your editor to Shift-JIS. And\n\twhile ZUN most definitely saw it as a \u003ccode\u003e¥\u003c/code\u003e, documenting this code\n\tpoint as \u003ccode\u003e\\\u003c/code\u003e is less ambiguous in the long run. It can only\n\tpossibly correspond to one specific code point in either Shift-JIS or UTF-8,\n\tand will remain correct even if we later mod the cutscene system to support\n\tfull-blown Unicode.\n\u003c/p\u003e\u003cp\u003e\n\tNow we've only got to clarify the parameter syntax, and then we can look at\n\tthe big table of commands:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eNumeric parameters are read as sequences of up to 3 ASCII digits. This\n\tlimits them to a range from 0 to 999 inclusive, with \u003ccode\u003e000\u003c/code\u003e and\n\t\u003ccode\u003e0\u003c/code\u003e being equivalent. Because there's no further sentinel\n\tcharacter, any further digit from the 4\u003csup\u003eth\u003c/sup\u003e one onwards is\n\tinterpreted as regular text.\u003c/li\u003e\n\t\u003cli\u003eFilename parameters must be terminated with a space or newline and are\n\tlimited to 12 characters, which translates to 8.3 basenames without any\n\tdirectory component. Any further characters are ignored and displayed as\n\ttext as well.\u003c/li\u003e\n\t\u003cli\u003eEach .PI image can contain up to four 320×200 pictures (\"quarters\") for\n\tthe cutscene picture area. In the script commands, they are numbered like\n\tthis: \u003ctable class=\"_2022-11-30\"\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e0\u003c/td\u003e\u003ctd\u003e1\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003ctr\u003e\u003ctd\u003e2\u003c/td\u003e\u003ctd\u003e3\u003c/td\u003e\u003c/tr\u003e\n\t\u003c/table\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure id=\"cutscene-2022-11-30\" class=\"_2022-11-30\"\u003e\u003ctable class=\"vm\"\u003e\n\t\u003ctr id=\"2022-11-30-at\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\@\u003c/td\u003e\n\t\t\u003ctd\u003eClears both VRAM pages by filling them with VRAM color 0.\u003cbr\u003e 🐞\n\t\tIn TH03 and TH04, this command does not update the internal text area\n\t\tbackground used for unblitting. This bug effectively restricts usage of\n\t\tthis command to either the beginning of a script (before the first\n\t\tbackground image is shown) or its end (after no more new text boxes are\n\t\tstarted). \u003ca href=\"#2022-11-30-at-example\"\u003eSee the image below for an\n\t\texample of using it anywhere else.\u003c/a\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-b\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\\b\u003cvar class=\"default\"\u003e2\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eSets the font weight to a value between 0 (raw font ROM glyphs) to 3\n\t\t(very thicc). Specifying any other value has no effect.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e🐞 In TH04 and TH05, \u003ccode\u003e\\b3\u003c/code\u003e leads to glitched pixels when\n\t\trendering half-width glyphs due to a bug in the newly micro-optimized\n\t\tASM version of\n\t\t\u003ccode\u003e\u003ca href=\"/blog/2021-04-23\"\u003e📝 graph_putsa_fx()\u003c/a\u003e\u003c/code\u003e; \u003ca\n\t\thref=\"#2022-11-30-b-example\"\u003esee the image below for an example\u003c/a\u003e.\n\t\t\u003cbr\u003e\n\t\tIn these games, the parameter also directly corresponds to the\n\t\t\u003ccode\u003egraph_putsa_fx()\u003c/code\u003e effect function, removing the sanity check\n\t\tthat was present in TH03. In exchange, you can also access the four\n\t\tdissolve masks for the bold font (\u003ccode\u003e\\b2\u003c/code\u003e) by specifying a\n\t\tparameter between \u003cvar\u003e4\u003c/var\u003e (fewest pixels) to \u003cvar\u003e7\u003c/var\u003e (most\n\t\tpixels). \u003ca href=\"#2022-11-30-dissolve\"\u003eDemo video below.\u003c/a\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-c\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\c\u003cvar class=\"default\"\u003e15\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eChanges the text color to VRAM color \u003cvar\n\t\tclass=\"default\"\u003e15\u003c/var\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\c=\u003cvar\u003e字\u003c/var\u003e,\u003cvar class=\"default\"\u003e15\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eAdds a color map entry: If \u003cvar\u003e字\u003c/var\u003e is the first code point\n\t\tinside the name area on a new line, the text color is automatically set\n\t\tto \u003cvar class=\"default\"\u003e15\u003c/var\u003e. Up to 8 such entries can be registered\n\t\tbefore overflowing the statically allocated buffer.\u003cbr\u003e\n\t\t🐞 The comma is assumed to be present even if the color parameter is omitted.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\e\u003cvar\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003ePlays the sound effect with the given ID.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\f\u003c/td\u003e\n\t\t\u003ctd\u003e(no-op)\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t\\fi\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003cbr\u003e\n\t\t\t\\fo\u003cvar class=\"default\"\u003e1\u003c/var\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003eCalls master.lib's \u003ccode\u003epalette_black_\u003cb\u003ei\u003c/b\u003en()\u003c/code\u003e or\n\t\t\u003ccode\u003epalette_black_\u003cb\u003eo\u003c/b\u003eut()\u003c/code\u003e to play a hardware palette fade\n\t\tanimation from or to black, spending roughly \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e frame on each of the 16 fade steps.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\fm\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eFades out BGM volume via PMD's \u003ccode\u003eAH=02h\u003c/code\u003e interrupt call,\n\t\tin a non-blocking way. The fade speed can range from \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e (slowest) to \u003cvar\u003e127\u003c/var\u003e (fastest).\u003cbr\u003e\n\t\tValues from \u003cvar\u003e128\u003c/var\u003e to \u003cvar\u003e255\u003c/var\u003e technically correspond to\n\t\t\u003ccode\u003eAH=02h\u003c/code\u003e's fade-in feature, which can't be used from cutscene\n\t\tscripts because it requires BGM volume to first be lowered via\n\t\t\u003ccode\u003eAH=19h\u003c/code\u003e, and there is no command to do that.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\g\u003cvar class=\"default\"\u003e8\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003ePlays a blocking \u003cvar class=\"default\"\u003e8\u003c/var\u003e-frame screen shake\n\t\tanimation.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\ga\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gaiji\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/a\u003e\u003c/span\u003e with the given ID from 0 to 255\n\t\tat the current cursor position. Even in TH03, gaiji always ignore the\n\t\ttext delay interval configured with \u003ca\n\t\thref=\"#2022-11-30-v-th03\"\u003e\u003ccode\u003e\\v\u003c/code\u003e\u003c/a\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@\u003cvar class=\"default\"\u003e3\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eTH05's replacement for the \u003ccode\u003e\\ga\u003c/code\u003e command from TH03 and\n\t\tTH04. The default ID of \u003cvar class=\"default\"\u003e3\u003c/var\u003e corresponds to the\n\t\t\u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAAAAP///yH5BAUAAAEALAAAAAAQABAAAAIojA15y5H/GoK0gVXZtfso2U2So40kNn5QemXWFHnK5lJdDOPgasJLAQA7\"\n\t\talt=\"♫\"\u003e gaiji. Not to be confused with \u003ca\n\t\thref=\"#2022-11-30-at\"\u003e\u003ccode\u003e\\@\u003c/code\u003e\u003c/a\u003e, which starts with a backslash,\n\t\tunlike this command.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@h\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIjjAN5y+n3oHsTyIotytjwvn1RKF5fUy6pyjEe6JJaTNKXbRQAOw==\"\n\t\talt=\"🎔\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@t\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIgjI+pywnfzptxUmavyqBqDEVGKJLBJ0mZo1rN+ooyUgAAOw==\"\n\t\talt=\"💦\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@!\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIejB8AyKja2lvRvWohNnPz63UhOHqHZqYnNaKYWDYFADs=\"\n\t\talt=\"!\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@?\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIgjI+pwK3HTIsrzWop0pLDDDyL9zhiR2bnyqaju5ltqBQAOw==\"\n\t\talt=\"?\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@!!\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIojANwqGuZHnQs1rZg05TfPn2O52VheZDhiDGUZL7ybMGghUr4GqdtAQA7\"\n\t\talt=\"‼\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e@!?\u003c/td\u003e\n\t\t\u003ctd\u003eShows the \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhEAAQAPABAAEBAQGB/yH5BAUAAAEALAAAAAAQABAAAAIqTACmu4jmXJPvREjZ3cymnUyfdGjP1XleaLKVyFpmStMv1qFr65LTvCgAADs=\"\n\t\talt=\"⁉\"\u003e gaiji.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-k\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\k\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eWaits \u003cvar class=\"default\"\u003e0\u003c/var\u003e frames (0 = forever) for an \u003ca\n\t\thref=\"#2022-11-30-advance\"\u003eadvance key\u003c/a\u003e to be pressed before\n\t\tcontinuing script execution. Before waiting, TH05 crossfades in any new\n\t\ttext that was previously rendered to the invisible VRAM page…\u003cbr\u003e\n\t\t🐞 …but TH04 doesn't, leaving the text invisible during the wait time.\n\t\tAs a workaround, \u003ca href=\"#2022-11-30-vp\"\u003e\u003ccode\u003e\\vp1\u003c/code\u003e\u003c/a\u003e can be\n\t\tused before \u003ccode\u003e\\k\u003c/code\u003e to immediately display that text without a\n\t\tfade-in animation.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\m$\u003c/td\u003e\n\t\t\u003ctd\u003eStops the currently playing BGM.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\m*\u003c/td\u003e\n\t\t\u003ctd\u003eRestarts playback of the currently loaded BGM from the\n\t\tbeginning.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\m,\u003cvar\u003efilename\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eStops the currently playing BGM, loads a new one from the given\n\t\tfile, and starts playback.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\n\u003c/td\u003e\n\t\t\u003ctd\u003eStarts a new line at the leftmost X coordinate of the box, i.e., the\n\t\tstart of the name area. This is how scripts can \"change\" the name of the\n\t\tcurrently speaking character, or use the entire 480×64 pixels without\n\t\tbeing restricted to the non-name area.\u003cbr\u003e\n\t\tNote that automatic line breaks already move the cursor into a new line.\n\t\tUsing this command at the \"end\" of a line with the maximum number of 30\n\t\tfull-width glyphs would therefore start a second new line and leave the\n\t\tpreviously started line empty.\u003cbr\u003e\n\t\tIf this command moved the cursor into the 5\u003csup\u003eth\u003c/sup\u003e line of a box,\n\t\t\u003ca href=\"#2022-11-30-s\"\u003e\u003ccode\u003e\\s\u003c/code\u003e\u003c/a\u003e is executed afterward, with\n\t\tany of \u003ccode\u003e\\n\u003c/code\u003e's parameters passed to \u003ccode\u003e\\s\u003c/code\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\p\u003c/td\u003e\n\t\t\u003ctd\u003e(no-op)\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-p-\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\p-\u003c/td\u003e\n\t\t\u003ctd\u003eDeallocates the loaded .PI image.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\p,\u003cvar\u003efilename\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eLoads the .PI image with the given file into the single .PI slot\n\t\tavailable to cutscenes. TH04 and TH05 automatically deallocate any\n\t\tprevious image, 🐞 TH03 would leak memory without a manual prior call to\n\t\t\u003ca href=\"#2022-11-30-p-\"\u003e\u003ccode\u003e\\p-\u003c/code\u003e\u003c/a\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\pp\u003c/td\u003e\n\t\t\u003ctd\u003eSets the hardware palette to the one of the loaded .PI image.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\p@\u003c/td\u003e\n\t\t\u003ctd\u003eSets the loaded .PI image as the full-screen 640×400 background\n\t\timage and overwrites both VRAM pages with its pixels, retaining the\n\t\tcurrent hardware palette.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\p=\u003c/td\u003e\n\t\t\u003ctd\u003eRuns \u003ccode\u003e\\pp\u003c/code\u003e followed by \u003ccode\u003e\\p@\u003c/code\u003e.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-s\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t\\s\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003cbr\u003e\n\t\t\t\\s-\u003cbr\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003eEnds a text box and starts a new one. Fades in any text rendered to\n\t\tthe invisible VRAM page, then waits \u003cvar class=\"default\"\u003e0\u003c/var\u003e frames\n\t\t(0 = forever) for an \u003ca href=\"#2022-11-30-advance\"\u003eadvance key\u003c/a\u003e to be\n\t\tpressed. Afterward, the new text box is started with the cursor moved to\n\t\tthe top-left corner of the name area.\u003cbr\u003e\n\t\t\u003ccode\u003e\\s-\u003c/code\u003e skips the wait time and starts the new box\n\t\timmediately.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\t\u003cvar class=\"default\"\u003e100\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eSets palette brightness via master.lib's\n\t\t\u003ccode\u003epalette_settone()\u003c/code\u003e to any value from 0 (fully black) to 200\n\t\t(fully white). 100 corresponds to the palette's original colors.\n\t\tPreceded by a 1-frame delay unless \u003ckbd\u003eESC\u003c/kbd\u003e is held.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-v-th03\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\\v\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eSets the number of frames to wait between every 2 bytes of rendered\n\t\ttext.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-v-th04\"\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003eSets the number of frames to spend on each of the 4 fade\n\t\tsteps when crossfading between old and new text. The game-specific\n\t\tdefault value is also used before the first use of this command.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003ccode\u003e\\v\u003cvar class=\"default\"\u003e2\u003c/var\u003e\u003c/code\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-vp\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\vp\u003cvar class=\"default\"\u003e0\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eShows VRAM page \u003cvar class=\"default\"\u003e0\u003c/var\u003e. Completely useless in\n\t\tTH03 (this game always synchronizes both VRAM pages at a command\n\t\tboundary), only of dubious use in TH04 (for working around a bug in \u003ca\n\t\thref=\"#2022-11-30-k\"\u003e\u003ccode\u003e\\k\u003c/code\u003e\u003c/a\u003e), and the games always return to\n\t\ttheir intended shown page before every blitting operation anyway. A\n\t\tdebloated mod of this game would just remove this command, as it exposes\n\t\tan implementation detail that script authors should not need to worry\n\t\tabout. None of the original scripts use it anyway.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-w\"\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\w\u003cvar class=\"default\"\u003e64\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"4\"\u003e\u003cul\u003e\n\t\t\t\u003cli\u003e\u003ccode\u003e\\w\u003c/code\u003e and \u003ccode\u003e\\wk\u003c/code\u003e wait for the given number\n\t\t\tof frames\u003c/li\u003e\n\t\t\t\u003cli\u003e\u003ccode\u003e\\wm\u003c/code\u003e and \u003ccode\u003e\\wmk\u003c/code\u003e wait until PMD has played\n\t\t\tback the current BGM for the total number of measures, including\n\t\t\tloops, given in the first parameter, and fall back on calling\n\t\t\t\u003ccode\u003e\\w\u003c/code\u003e and \u003ccode\u003e\\wk\u003c/code\u003e with the second parameter as\n\t\t\tthe frame number if BGM is disabled.\u003cbr\u003e\n\t\t\t🐞 Neither PMD nor MMD reset the internal measure when stopping\n\t\t\tplayback. If no BGM is playing and the previous BGM hasn't been\n\t\t\tplayed back for at least the given number of measures, this command\n\t\t\twill deadlock.\u003c/li\u003e\n\t\t\u003c/ul\u003e\n\t\tSince both TH04 and TH05 fade in any new text from the invisible VRAM\n\t\tpage, these commands can be used to simulate TH03's typing effect in\n\t\tthose games. \u003ca href=\"#2022-11-30-w-example\"\u003eDemo video below.\u003c/a\u003e\u003cbr\u003e\n\t\tContrary to \u003ca href=\"#2022-11-30-k\"\u003e\u003ccode\u003e\\k\u003c/code\u003e\u003c/a\u003e and \u003ca\n\t\thref=\"#2022-11-30-s\"\u003e\u003ccode\u003e\\s\u003c/code\u003e\u003c/a\u003e, specifying 0 frames would\n\t\tsimply remove any frame delay instead of waiting forever.\u003cbr\u003e\n\t\tThe TH03-exclusive \u003ccode\u003ek\u003c/code\u003e variants allow the delay to be\n\t\tinterrupted if \u003ckbd\u003e⏎ Return\u003c/kbd\u003e or \u003ckbd\u003eShot\u003c/kbd\u003e are held down.\n\t\tTH04 and TH05 recognize the \u003ccode\u003ek\u003c/code\u003e as well, but removed its\n\t\tfunctionality.\u003cbr\u003e\n\t\tAll of these commands have no effect if \u003ckbd\u003eESC\u003c/kbd\u003e is held.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003ccode\u003e\\wm\u003cvar class=\"default\"\u003e64\u003c/var\u003e,\u003cvar class=\"default\"\u003e64\u003c/var\u003e\u003c/code\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003c/td\u003e\n\t\t\u003ctd rowspan=\"2\"\u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003ccode\u003e\\wk\u003cvar class=\"default\"\u003e64\u003c/var\u003e\u003c/code\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003ccode\u003e\\wmk\u003cvar class=\"default\"\u003e64\u003c/var\u003e,\u003cvar class=\"default\"\u003e64\u003c/var\u003e\u003c/code\u003e\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t\\wi\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003cbr\u003e\n\t\t\t\\wo\u003cvar class=\"default\"\u003e1\u003c/var\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003eCalls master.lib's \u003ccode\u003epalette_white_\u003cb\u003ei\u003c/b\u003en()\u003c/code\u003e or\n\t\t\u003ccode\u003epalette_white_\u003cb\u003eo\u003c/b\u003eut()\u003c/code\u003e to play a hardware palette fade\n\t\tanimation from or to white, spending roughly \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e frame on each of the 16 fade steps.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\=\u003cvar class=\"default\"\u003e4\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eImmediately displays the given quarter of the loaded .PI image in\n\t\tthe picture area, with no fade effect. Any value ≥\u0026nbsp;\u003cvar\n\t\tclass=\"default\"\u003e4\u003c/var\u003e resets the picture area to black.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\==\u003cvar class=\"default\"\u003e4\u003c/var\u003e,\u003cvar class=\"default\"\u003e1\u003c/var\u003e\u003c/td\u003e\n\t\t\u003ctd\u003eCrossfades the picture area between its current content and quarter\n\t\t#\u003cvar class=\"default\"\u003e4\u003c/var\u003e of the loaded .PI image, spending \u003cvar\n\t\tclass=\"default\"\u003e1\u003c/var\u003e frame on each of the 4 fade steps unless\n\t\t\u003ckbd\u003eESC\u003c/kbd\u003e is held. Any value ≥\u0026nbsp;\u003cvar class=\"default\"\u003e4\u003c/var\u003e is\n\t\treplaced with quarter #0.\u003c/td\u003e\n\t\u003c/tr\u003e\u003ctr id=\"2022-11-30-$\"\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th03.png?2d64c5db\" alt=\":th03:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th04.png?58cec89f\" alt=\":th04:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\u003cimg src=\"/static/emoji-th05.png?defa204a\" alt=\":th05:\" width=\"24\" height=\"24\" \u003e\u003c/td\u003e\n\t\t\u003ctd\u003e\\$\u003c/td\u003e\n\t\t\u003ctd\u003eStops script execution. Must be called at the end of each file;\n\t\totherwise, execution continues into whatever lies after the script\n\t\tbuffer in memory.\u003cbr\u003e\n\t\tTH05 automatically deallocates the loaded .PI image, TH03 and TH04\n\t\trequire a separate manual call to \u003ca\n\t\thref=\"#2022-11-30-p-\"\u003e\u003ccode\u003e\\p-\u003c/code\u003e\u003c/a\u003e to not leak its memory.\u003c/td\u003e\n\t\u003c/tr\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\t\u003cvar class=\"default\"\u003eBold\u003c/var\u003e values signify the default if the parameter\n\tis omitted; \u003ca href=\"#2022-11-30-c\"\u003e\u003ccode\u003e\\c\u003c/code\u003e\u003c/a\u003e is therefore\n\tequivalent to \u003ccode\u003e\\c15\u003c/code\u003e.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cfigure id=\"2022-11-30-at-example\" class=\"fullres\"\u003e\u003cimg\n\tsrc=\"/blog/static/2022-11-30-Cutscene-@-bug.png?4c3eafce\"\n\talt=\"Using the \\@ command in the middle of a TH03 or TH04 cutscene script\"\n\u003e\u003cfigcaption\u003e\n\tThe \u003ca href=\"#2022-11-30-at\"\u003e\u003ccode\u003e\\@\u003c/code\u003e\u003c/a\u003e bug. Yes, the ¥ is fake. It\n\twas easier to GIMP it than to reword the sentences so that the backslashes\n\tlanded on the second byte of a 2-byte half-width character pair.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cfigure id=\"2022-11-30-b-example\" class=\"fullres\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-TH03-Cutscene-b.png?01992ff5\"\n\t\tdata-title='TH03'\n\t\talt=\"Cutscene font weights in TH03\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-TH05-Cutscene-b.png?dee4cf53\"\n\t\tdata-title=\"TH04 / TH05\"\n\t\talt=\"Cutscene font weights in TH05, demonstrating the \u003ccode\u003e\\b3\u003c/code\u003e bug that also affects TH04\"\n\t\tclass=\"active\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-TH03-Cutscene-b-unaligned.png?2fbafda7\"\n\t\tdata-title='TH03 (unaligned)'\n\t\talt=\"Cutscene font weights in TH03, rendered at a hypothetical unaligned X position\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-11-30-TH05-Cutscene-b-unaligned.png?f05bac90\"\n\t\tdata-title=\"TH04 / TH05 (unaligned)\"\n\t\talt=\"Cutscene font weights in TH05, rendered at a hypothetical unaligned X position\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\u003cfigcaption\u003e\n\t\tThe font weights and effects available through \u003ca\n\t\thref=\"#2022-11-30-b\"\u003e\u003ccode\u003e\\b\u003c/code\u003e\u003c/a\u003e, including the glitch with\n\t\t\u003ccode\u003e\\b3\u003c/code\u003e in TH04 and TH05.\u003cbr\u003e\n\t\tFont weight 3 is technically not rendered correctly in TH03 either; if\n\t\tyou compare 1️⃣ with 4️⃣, you notice a single missing column of pixels\n\t\tat the left side of each glyph, which would extend into the previous\n\t\tVRAM byte. Ironically, the TH04/TH05 version is more \u003ci\u003ecorrect\u003c/i\u003e in\n\t\tthis regard: For half-width glyphs, it preserves any further pixel\n\t\tcolumns generated by the weight functions in the high byte of the 16-dot\n\t\tglyph variable. Unlike TH03, which still cuts them off when rendering\n\t\ttext to unaligned X positions (3️⃣), TH04 and TH05 do bit-rotate them\n\t\ttowards their correct place (4️⃣). It's only at byte-aligned X positions\n\t\t(2️⃣) where they remain at their internally calculated place, and appear\n\t\ton screen as these glitched pixel columns, 15 pixels away from the glyph\n\t\tthey belong to. It's easy to blame bugs like these on micro-optimized\n\t\tASM code, but in this instance, you really can't argue against it if the\n\t\toriginal C++ version was equally incorrect.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cfigure id=\"2022-11-30-dissolve\" style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-11-30-TH05-Cutscene-dissolve-animation.webp?06cff739\" preload=\"none\" controls loop width=\"640\" height=\"160\" data-fps=\"56.423132\" data-frame-count=\"94\" style=\"aspect-ratio: 640 / 160\" data-lossless=\"/blog/static/video/zmbv/2022-11-30-TH05-Cutscene-dissolve-animation.avi?9a394eb4\"\u003e\u003csource src=\"/blog/static/video/av1/2022-11-30-TH05-Cutscene-dissolve-animation.webm?e61a7854\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-11-30-TH05-Cutscene-dissolve-animation.webm?88530a56\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-11-30-TH05-Cutscene-dissolve-animation.webm?d5c20d07\" type=\"video/webm\"\u003eDemo of using the text dissolve masks in TH04 and TH05 inside a cutscene script to fake an animation on only a part of the text. \u003ca href=\"/blog/static/video/zmbv/2022-11-30-TH05-Cutscene-dissolve-animation.avi?9a394eb4\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"3\" data-title=\"\u003ccode\u003e\u0026bsol;b4\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"11\" data-title=\"\u003ccode\u003e\u0026bsol;b5\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"19\" data-title=\"\u003ccode\u003e\u0026bsol;b6\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"27\" data-title=\"\u003ccode\u003e\u0026bsol;b7\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"35\" data-title=\"\u003ccode\u003e\u0026bsol;b2\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"71\" data-title=\"\u003ccode\u003e\u0026bsol;b7\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"78\" data-title=\"\u003ccode\u003e\u0026bsol;b6\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"83\" data-title=\"\u003ccode\u003e\u0026bsol;b5\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"89\" data-title=\"\u003ccode\u003e\u0026bsol;b4\u003c/code\u003e\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tCombining \u003ca href=\"#2022-11-30-b\"\u003e\u003ccode\u003e\\b\u003c/code\u003e\u003c/a\u003e and \u003ca\n\t\thref=\"#2022-11-30-s\"\u003e\u003ccode\u003es-\u003c/code\u003e\u003c/a\u003e into a partial dissolve\n\t\tanimation. The speed can be controlled with \u003ca\n\t\thref=\"#2022-11-30-v-th04\"\u003e\u003ccode\u003e\\v\u003c/code\u003e\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cfigure id=\"2022-11-30-w-example\" style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-11-30-TH05-Cutscene-w.webp?a5803601\" preload=\"none\" controls loop width=\"640\" height=\"128\" data-fps=\"56.423132\" data-frame-count=\"283\" style=\"aspect-ratio: 640 / 128\" data-lossless=\"/blog/static/video/zmbv/2022-11-30-TH05-Cutscene-w.avi?273b9c66\"\u003e\u003csource src=\"/blog/static/video/av1/2022-11-30-TH05-Cutscene-w.webm?a04b17c9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-11-30-TH05-Cutscene-w.webm?7893f5b2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-11-30-TH05-Cutscene-w.webm?7b1b1636\" type=\"video/webm\"\u003eDemo of using \u003ccode\u003e\\w\u003c/code\u003e to simulate TH03's typing effect in TH04 and TH05. \u003ca href=\"/blog/static/video/zmbv/2022-11-30-TH05-Cutscene-w.avi?273b9c66\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tSimulating TH03's typing effect in TH04 and TH05 via \u003ca\n\t\thref=\"#2022-11-30-w\"\u003e\u003ccode\u003e\\w\u003c/code\u003e\u003c/a\u003e. Even prettier in TH05 where we\n\t\talso get an additional \u003ca href=\"#2022-11-30-oldtext\"\u003efade animation\u003c/a\u003e\n\t\tafter the box ends.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo yeah, that's the cutscene system. I'm dreading the moment I will have to\n\tdeal with the \u003ci\u003eother\u003c/i\u003e command interpreter in these games, i.e., the\n\tstage enemy system. Luckily, that one is completely disconnected from any\n\tother system, so I won't have to deal with it until we're close to finishing\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e… that is, unless someone requests it before. And it\n\twon't involve text encodings or unblitting…\n\u003c/p\u003e\u003chr id=\"translations-2022-11-30\"\u003e\u003cp\u003e\n\tThe cutscene system got me thinking in greater detail about how I would\n\timplement translations, being one of the main dependencies behind them. This\n\tgoal has been on the order form for a while and could soon be implemented\n\tfor these cutscenes, with 100% PI being right around the corner for the TH03\n\tand TH04 cutscene executables.\u003cbr\u003e\n\tOnce we're there, the \"Virgin\" old-school way of static translation patching\n\tfor Latin-script languages could be implemented fairly quickly:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eEstablish basic UTF-8 parsing for less painful manual editing of the\n\tsource files\u003c/li\u003e\n\t\u003cli\u003eProcedurally generate glyphs for the few required additional letters\n\tbased on existing font ROM glyphs. For example, we'd generate \u003ccode\u003eä\u003c/code\u003e\n\tby painting two short lines on top of the font ROM's \u003ccode\u003ea\u003c/code\u003e glyph,\n\tor generate \u003ccode\u003e¿\u003c/code\u003e by vertically flipping the question mark. This\n\tway, the text retains a consistent look regardless of whether the translated\n\tgame is run with an NEC or EPSON font ROM, or the \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhlgALAPABAAAAAAAAACH5BAUKAAEALAAAAACWAAsAAALiRI4IlskNo5xUvRqu0xjy1TWcKI3epJnRpWYb9bSf+Tnkkq74xu7KbwHBcAyD70AMCoG/Yyb5hCI9RyPIx6I+LdOtdxljloDCcrHoWrpe3rV7nWWbkVU280y2vfEdMD0P2PUnxuemxkUo2LYX1xX3qBeFARkIF7lIKXg4qIiG2NloOZj5triiR+oJeqlqWBLaqtbD+ucXuLlJu9epOZu4q2gH7Buc8zVDxmf7OopoG0byO8cZzRkbY9wEdigliW16giWpdHfibFPVXSpV9mX+QvxZ6YRi5KhVXn0PL1LTH22gAAA7\"\n\talt=\"hideous abomination\"\u003e that Neko Project II auto-generates if you\n\tdon't provide either.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003e(Optional)\u003c/i\u003e Change automatic line breaks to work on a per-word\n\tbasis, rather than per-glyph\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThat's it – script editing and distribution would be handled by your local\n\ttranslation group. It might seem as if this would also work for Greek and\n\tCyrillic scripts due to their presence in the PC-98 font ROM, but I'm not\n\tsure if I want to attempt procedurally shrinking these glyphs from 16×16 to\n\t8×16… For any more thorough solution, we'd need to go for a more \"Chad\" kind\n\tof full-blown translation support:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eImplement text subdivisions at a sensible granularity while retaining\n\tautomatic line and box breaks\u003c/li\u003e\n\t\u003cli\u003eCompile translatable text into a Japanese→target language dictionary\n\t(I'm too old to develop any further translation systems that would overwrite\n\tmodded source text with translations of the original text)\u003c/li\u003e\n\t\u003cli\u003eImplement a custom Unicode font system (glyphs would be taken from GNU\n\tUnifont unless translators provide a different 8×16 font for their\n\tlanguage)\u003c/li\u003e\n\t\u003cli\u003eCombine the text compiler with the font compiler to only store needed\n\tglyphs as part of the translation's font file (dealing with a multi-MB font\n\tfile would be rather ugly in a Real Mode game)\u003c/li\u003e\n\t\u003cli\u003eWrite a simple install/update/patch stacking tool that supports both\n\t.HDI and raw-file DOSBox-X scenarios (it's different enough from thcrap to\n\twarrant a separate tool – each patch stack would be statically compiled into\n\ta single package file in the game's directory)\u003c/li\u003e\n\t\u003cli\u003eAdd a nice language selection option to the main menu\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003e(Optional)\u003c/i\u003e Support proportional fonts\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tWhich sounds more like a separate project to be commissioned from\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e's Open Collective funds, separate from the ReC98 cap.\n\tThis way, we can make sure that the feature is completely implemented, and I\n\tcan talk with every interested translator to make sure that their language\n\tworks.\u003cbr\u003e\n\tIt's still cheaper overall to do this on PC-98 than to first port the games\n\tto a modern system and \u003ci\u003ethen\u003c/i\u003e translate them. On the other hand, most\n\tof the tasks in the Chad variant (3, 4, 5, and half of 2) purely deal with\n\tthe difficulty of getting arbitrary Unicode characters to work natively in a\n\tPC-98 DOS game at all, and would be either unnecessary or trivial if we had\n\talready ported the game. Depending on where the patrons' interests lie, it\n\t\u003ci\u003emay\u003c/i\u003e not be worth it. So let's see what all of you think about  which\n\tway we should go\u003cs\u003e, or whether it's worth doing at all\u003c/s\u003e. (\u003cstrong\u003eEdit\n\t(2022-12-01):\u003c/strong\u003e With \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e's \u003cscript\u003eformatCurrency(6000)\u003c/script\u003e\u003cnoscript\u003e60.00\u0026nbsp;€\u003c/noscript\u003e\n\torder towards the stage dialogue system, we've pretty much confirmed that it\n\tis.) Maybe we want to meet in the middle – using e.g. procedural glyph\n\tgeneration for dynamic translations to keep text rendering consistent with\n\tthe rest of the PC-98 system, and just not support non-Latin-script\n\tlanguages in the beginning? In any case, I've added both options to the\n\torder form.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2023-07-28):\u003c/strong\u003e \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e has agreed to fund\n\ta basic feature set somewhere between the Virgin and Chad level. Check the\n\t\u003ca href=\"/blog/2023-07-28\"\u003e📝 dedicated announcement blog post\u003c/a\u003e for more\n\tdetails and ideas, and to find out how you can support this goal!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSurprisingly, there was still a bit of RE work left in the third push after\n\tall of this, which I filled with some small rendering boilerplate. Since I\n\talso wanted to include TH02's playfield overlay functions,\n\t\u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e15\u003c/sub\u003e of that last push went towards getting a\n\tTH02-exclusive function out of the way, which also ended up including that\n\tgame in this delivery. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThe other small function pointed out how TH05's Stage 5 midboss pops into\n\tthe playfield quite suddenly, since its clipping test thinks it's only 32\n\tpixels tall rather than 64:\n\u003c/p\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-11-30-TH05-Stage-5-midboss-pop-in.webp?646dfcb7\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"208\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-11-30-TH05-Stage-5-midboss-pop-in.avi?b44f5ff5\"\u003e\u003csource src=\"/blog/static/video/av1/2022-11-30-TH05-Stage-5-midboss-pop-in.webm?8c3cb967\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-11-30-TH05-Stage-5-midboss-pop-in.webm?0bf08b70\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-11-30-TH05-Stage-5-midboss-pop-in.webm?c00ca5ce\" type=\"video/webm\"\u003eVideo of the TH05 Stage 5 midboss pop-in quirk. \u003ca href=\"/blog/static/video/zmbv/2022-11-30-TH05-Stage-5-midboss-pop-in.avi?b44f5ff5\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"64\" data-title=\"Midboss appears\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\t\u003cs\u003eGood chance that the pop-in might have been intended.\u003c/s\u003e\u003cbr\u003e\n\t\t\u003cstrong\u003eEdit (2023-06-30):\u003c/strong\u003e Actually, it's a\n\t\t\u003ca href=\"/blog/2023-06-30\"\u003e📝 systematic consequence of ZUN having to work around the lack of clipping in master.lib's sprite functions\u003c/a\u003e.\n\t\t\u003cbr\u003e\n\t\tThere's even another quirk here: The white flash during its first frame\n\t\tis actually carried over from the \u003ci\u003eprevious\u003c/i\u003e midboss, which the\n\t\tgame still considers as actively getting hit by the player shot that\n\t\tdefeated it. It's the regular boilerplate code for \u003ci\u003erendering\u003c/i\u003e a\n\t\tmidboss that resets the responsible damage variable, and that code\n\t\tdoesn't run during the defeat explosion animation.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNext up: Staying with TH05 and looking at more of the pattern code of its\n\tboss fights. Given the remaining TH05 budget, it makes the most sense to\n\tcontinue in in-game order, with Sara and the Stage 2 midboss. If more money\n\tcomes in towards this goal, I could alternatively go for the Mai \u0026 Yuki\n\tfight and immediately develop a pretty fix for the \u003ca\n\thref=\"https://www.youtube.com/watch?v=b1k82w1VzUc\"\u003echeeto storage\n\tglitch\u003c/a\u003e. Also, there's \u003ca\n\thref=\"https://github.com/nmlgc/rec98.nmlgc.net/pull/9\"\u003ea rather intricate\n\tpull request for direct ZMBV decoding on the website\u003c/a\u003e that I've still got\n\tto review…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-12-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-10-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-11-30T22:20:03Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-10-31",
      "url": "https://rec98.nmlgc.net/blog/2022-10-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-09-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-10-31\"\u003e\u003ctime datetime=\"2022-10-31T23:58:39Z\"\u003e2022-10-31 23:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0218\"\u003eP0218\u003c/a\u003e\n\t\t\tWebsite (Video pipeline, part 1/2: Preparations / Source file gathering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/21f0a4d...8ebf201\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0219\"\u003eP0219\u003c/a\u003e\n\t\t\tWebsite (Video pipeline, part 2/2: Encoding / Tweaking settings / Caching)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/8ebf201...52375e2\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0220\"\u003eP0220\u003c/a\u003e\n\t\t\tWebsite (Video player, part 1/3: Basic controls and frame-accurate seeking)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/52375e2...ba6359b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0221\"\u003eP0221\u003c/a\u003e\n\t\t\tWebsite (Video player, part 2/3: Tabs and markers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/ba6359b...94e48e9\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0222\"\u003eP0222\u003c/a\u003e\n\t\t\tWebsite (Video player, part 3/3: Dynamic captions and useful fullscreen mode)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/94e48e9...358e16f\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Yanga, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#singleplayer_playfield-2022-10-31 img {\n\t\twidth: 768px;\n\t}\n\t#trials-2022-10-31 {\n\t\tfont-size: 75%;\n\t}\n\t#trials-2022-10-31 tbody td:not(:nth-child(4)) {\n\t\ttext-align: right;\n\t\twhite-space: nowrap;\n\t}\n\t#trials-2022-10-31 tbody td:nth-child(4) {\n\t\ttext-align: left;\n\t\tfont-family: monospace;\n\t}\n\t#trials-2022-10-31 tbody {\n\t\tcolor: #444;\n\t}\n\t#trials-2022-10-31 tbody .active {\n\t\tfont-weight: bold;\n\t\tcolor: black;\n\t}\n\t#trials-2022-10-31 .av1 {\n\t\tbackground-color: var(--c-trial-good);\n\t}\n\t#trials-2022-10-31 .vp8 {\n\t\tbackground-color: var(--c-trial-mid);\n\t}\n\t#trials-2022-10-31 .vp9 {\n\t\tbackground-color: var(--c-trial-bad);\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tYes, I'm still alive. This delivery was just plagued by all of the worst\n\tluck: Data loss, physical hard drive failure, exploding phone batteries,\n\tminor illness… and after taking 4 weeks to recover from all of that, I had\n\tto face \u003ci\u003ethis\u003c/i\u003e beast of a task. 😵\n\u003c/p\u003e\u003cp\u003e\n\tTurns out that neither part of improving video performance and usability on\n\tthis blog was particularly easy. Decently encoding the videos into all\n\tweb-supported formats required unexpected trade-offs even for the low-res,\n\tlow-color material we are working with, and writing custom video player\n\tcontrols added the timing precision resistance of HTML\n\t\u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e on top of the inherent complexity of frontend web\n\tdevelopment. Why did this need to be 800 lines of commented JavaScript and\n\t200 lines of commented CSS, and consume almost more than 5 pushes?!\n\tApparently, the latest price increase also seemed to have raised the minimum\n\tlevel of acceptable polish in my work, since that's more than the maximum of\n\t3.67 pushes it should have taken. To fund the rest, I stole some of the\n\treserved JIS trail word rendering research pushes, which means that the next\n\t\u003cscript\u003eformatCurrency(10000)\u003c/script\u003e\u003cnoscript\u003e100.00\u0026nbsp;€\u003c/noscript\u003e towards anything will go back towards that goal.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe codec situation is especially sad because it seems like so much of a\n\tsolved problem. ZMBV, the lossless capture codec introduced by DOSBox, is\n\tboth very well suited for retro game footage and remarkably simple too:\n\tDOSBox-X's implementation of both an encoder and decoder comes in at under\n\t650 lines of C++, excluding the Deflate implementation. Heck, the AVI\n\tcontainer \u003ci\u003earound\u003c/i\u003e the codec is more complicated to write than the\n\tcompressed video data itself, and AVI is already the easiest choice you have\n\tfor a widely supported video container format.\u003cbr\u003e\n\tCurrently, this blog contains 9:02 minutes of video across 86 files, with a\n\ttotal frame count of 24,515. In case this post attracts a general video\n\tencoding audience that isn't familiar with what I'm encoding here: The\n\tmaximum resolution is 640×400, and most of the video uses 16 colors, with\n\tsome parts occasionally using more. With ZMBV, the lossless source files\n\ttake up 43.8\u0026nbsp;MiB, and that's even \u003ci\u003ewith\u003c/i\u003e AVI's infamously bad\n\toverhead. While you can always spend more time on any compression task and\n\tprecisely tune your algorithm to match your source data even better,\n\t43.8\u0026nbsp;MiB looks like a more than reasonable amount for this type of\n\tcontent.\n\u003c/p\u003e\u003cp\u003e\n\tEspecially compared with what I actually have to ship here, because sadly,\n\tZMBV is not supported by browsers. 😔 Writing a WebAssembly player for ZMBV\n\twould have certainly been interesting, but it already took 5 pushes to get\n\tto what we have now. So, let's instead shell out to ffmpeg and build a\n\tpipeline to convert ZMBV to the ill-suited codecs supported by web browsers,\n\treplacing the previously committed VP9 and VP8 files. From that point, we\n\tcan then look into AV1, the latest and greatest web-supported video codec,\n\tto save some additional bandwidth.\n\u003c/p\u003e\u003cp\u003e\n\tBut first, we've got to gather all the ZMBV source files. While I was\n\tworking on the \u003ca href=\"/blog/2022-07-10\"\u003e📝 2022-07-10 blog post\u003c/a\u003e, I\n\tnoticed some weirdly washed-out colors in the converted videos, leading to\n\tthe shocking realization that my previous, historically grown conversion\n\tscript didn't \u003ci\u003eactually\u003c/i\u003e encode in a lossless way. 😢 By extension,\n\tthis meant that every video before that post could have had minor\n\tdiscolorations as well.\u003cbr\u003e\n\tFor the majority of videos, I still had the original ZMBV capture files\n\tstraight out of DOSBox-X, and reproducing the final videos wasn't too big of\n\ta deal. For the few cases where I didn't, I went the extra mile, took the\n\tVP9 files, and manually fixed up all the minor color errors based on\n\treference videos from the same gameplay stage. There might be a huge ffmpeg\n\tcommand line with a complicated filter graph to do the job, but for such a\n\tsmall 4-digit number of frames, it is much more straightforward to just dump\n\teach frame as an image and perform the color replacement with ImageMagick's\n\t\u003ccode\u003e-opaque\u003c/code\u003e and \u003ccode\u003e-fill\u003c/code\u003e options.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, time to encode our new definite collection of source files into AV1, and\n\t\u003ci\u003ewhat the hell\u003c/i\u003e, how slow \u003ci\u003eis\u003c/i\u003e this codec? With ffmpeg's\n\t\u003ccode\u003elibaom-av1\u003c/code\u003e, fully encoding all 86 videos takes \u003ci\u003ealmost 9\n\thours\u003c/i\u003e on my \u003ca\n\thref=\"https://twitter.com/lunasorcery/status/1578483707979014144\"\u003emid-range\n\tdevelopment system\u003c/a\u003e, regardless of the quality selected.\u003cbr\u003e\n\tBut sure, the encoded videos are managed by a cache, and this obviously only\n\tneeds to be done once. If the results are amazing, they might even justify\n\tthese glacial encoding speeds. Unfortunately, they don't: In its lossless\n\t\u003ccode\u003e-crf 0\u003c/code\u003e mode, AV1 performs even \u003ci\u003eworse\u003c/i\u003e than VP9, taking up\n\t222\u0026nbsp;MiB rather than 182\u0026nbsp;MiB. It might not \u003ci\u003esound\u003c/i\u003e bad now,\n\tbut as we're later going to find out, we want to have a \u003ci\u003elot\u003c/i\u003e of\n\tkeyframes in these videos, which will blow up video sizes even further.\n\u003c/p\u003e\u003cp\u003e\n\tSo, time to go lossy and maybe take a deep dive into AV1 tuning? Turns out\n\tthat it only gets worse from there:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe alternative \u003ccode\u003elibsvtav1\u003c/code\u003e encoder is fast and creates small\n\tfiles… but even on the highest-quality settings, \u003ccode\u003e-crf 0\u003c/code\u003e and\n\t\u003ccode\u003e-qp 0\u003c/code\u003e, the video quality resembled the terrible x264 YUV420P\n\tformat that Twitter enforces on uploaded videos.\u003c/li\u003e\n\t\u003cli\u003eI don't remember the \u003ccode\u003elibrav1e\u003c/code\u003e results, but they sure\n\tweren't convincing either.\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003elibaom-av1\u003c/code\u003e's \u003ccode\u003e-usage realtime\u003c/code\u003e option is a\n\tcomplete joke. 771\u0026nbsp;MiB for all videos, and it doesn't even compress\n\t\u003ci\u003ein\u003c/i\u003e real time on my system, more like 2.5× real-time. For comparison,\n\ta certain stone-age technology by the name of \"animated GIF\" would take\n\t54.3\u0026nbsp;MiB, encode in sub-realtime (0.47×), and the only necessary tuning\n\tyou need is an \u003ca\n\thref=\"https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/\"\u003eeasily\n\tgoogled palette generation and usage filter\u003c/a\u003e. Why can't I just use\n\t\u003ci\u003ethose\u003c/i\u003e in a \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e tag?! These results have\n\tclearly proven the top-voted \u003cq\u003ejust use modern video codecs\u003c/q\u003e Stack\n\tOverflow answers wrong.\u003c/li\u003e\n\t\u003cli\u003eWhat you're actually supposed to do is to drop \u003ccode\u003e-cpu-used\u003c/code\u003e to\n\tmaybe 2 or 3, and then selectively add back prediction filters that suit\n\tyour type of content. In our case, these are\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003e-enable-palette\u003c/code\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003e-enable-rect-partitions\u003c/code\u003e and friends\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003e-enable-intrabc\u003c/code\u003e (\u003ca\n\t\thref=\"https://thebroadcastknowledge.com/2020/11/05/video-av1-real-time-screen-content-coding/#video\"\u003esource\u003c/a\u003e)\u003c/li\u003e\n\t\u003c/ul\u003e and maybe others, depending on much time you want to waste.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBecause that's what all this tuning ended up being: a complete waste of\n\ttime. No matter which tuning options I tried, all they did was cut down\n\tencoding time in exchange for slightly larger files on average. If there is\n\ta magic tuning option that would suddenly cause AV1 to maybe even beat ZMBV,\n\tI haven't found it. Heck, at particularly low settings,\n\t\u003ccode\u003e-enable-intrabc\u003c/code\u003e even caused blocky glitches with certain pellet\n\tpatterns that looked like the internal frame block hashes were colliding all\n\tover the place. Unfortunately, I didn't save the video where it happened.\n\u003c/p\u003e\u003cp\u003e\n\tSo yeah, if you've already invested the computation time and encoded your\n\tcontent by just specifying a \u003ccode\u003e-crf\u003c/code\u003e value and keeping the\n\tremaining settings at their time-consuming defaults, any further tuning will\n\tmake no difference. Which is… an interesting choice from a usability\n\tperspective. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e I would have expected the exact\n\topposite: default to a reasonably fast and efficient profile, and leave the\n\tvast selection of tuning options for those people to explore who \u003ci\u003edo\u003c/i\u003e\n\twant to wait 5× as long for their encoder for that additional 5% of\n\tcompression efficiency. On the other hand, that surely is one way to get\n\tpeople to extensively study your glorious engineering efforts, I guess? You\n\tknow what would maybe even \u003ci\u003emotivate\u003c/i\u003e people to intrinsically do that?\n\tGood documentation, with examples of the intent behind every option and its\n\toptimal use case. Nobody needs long help strings that just spell out all of\n\tthe abbreviations that occur in the name of the option…\u003cbr\u003e\n\tBut hey, that at least means there's no reason to not use anything but ZMBV\n\tfor storing and archiving the lossless source files. Best compression\n\tefficiency, encodes in real-time, and the files are much easier to edit.\n\u003c/p\u003e\u003cp\u003e\n\tOK, end of rant. To understand why \u003ca\n\thref=\"https://reddit.com/r/AV1\"\u003eanyone\u003c/a\u003e could be hyped about AV1 to begin\n\twith, we just have to compare it to VP9, not to ZMBV. In that light, AV1\n\t\u003ci\u003eis\u003c/i\u003e pretty impressive even at \u003ccode\u003e-crf 1\u003c/code\u003e, compressing all 86\n\tvideos to 68.9\u0026nbsp;MiB, and even preserving 22.3% of frames completely\n\tlosslessly. The remaining frames exhibit the exact kind of quality loss\n\tyou'd want for retro game footage: Minor discoloration in individual pixels,\n\tso minuscule that subtracting the encoded image from the source yields an\n\talmost completely black image. Even after highlighting the errors by\n\tnormalizing such a difference image, they are barely visible even if you\n\tknow where to look. If \"compressed PNG size of the normalized difference\n\tbetween ZMBV and AV1 \u003ccode\u003e-crf 1\u003c/code\u003e\" is a useful metric, this would be\n\tits median frame among the 77.7% of non-lossless frames:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Median-ZMBV.png?44d3c062\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"Lossless\"\n\t\talt=\"The lossless source image\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Median-AV1-crf-1.png?e04a434a\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"AV1 \u003ccode\u003e-crf 1\u003c/code\u003e\"\n\t\talt=\"The same image encoded in AV1\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Median-AV1-crf-1-difference.png?47e9289c\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"Normalized difference\"\n\t\talt=\"The normalized difference between both images\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tThat's frame 455 (0-based) of\n\t\t\u003ca href=\"/blog/2022-08-08\"\u003e📝 YuugenMagan's reconstructed Phase 5 pattern on Easy mode\u003c/a\u003e.\n\t\tThe AV1 version does in fact expand the original image's 16 distinct\n\t\tcolors to 38.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFor comparison, here's the 13th worst one. The codec only resorts to color\n\tbleeding with particularly heavy effects, exactly where it doesn't matter:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher id=\"singleplayer_playfield-2022-10-31\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Color-bleed-ZMBV.png?41aa9c2e\"\n\t\tdata-title=\"Lossless\"\n\t\talt=\"The lossless source image\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Color-bleed-AV1-crf-1.png?79871ebb\"\n\t\tdata-title=\"AV1 \u003ccode\u003e-crf 1\u003c/code\u003e\"\n\t\talt=\"The same image encoded in AV1\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Color-bleed-AV1-crf-1-difference.png?2d609f07\"\n\t\tdata-title=\"Normalized difference\"\n\t\talt=\"The normalized difference between both images\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tFrame 25 (0-based) of the\n\t\t\u003ca href=\"/blog/2020-08-28\"\u003e📝 TH05 Reimu bomb animation quirk video\u003c/a\u003e.\n\t\t139 colors in the AV1 version.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhether you can actually spot the difference is pretty much down to the\n\tglass between the physical pixels and your eyes. In any case, it's very\n\thard, even if you know where to look. As far as I'm concerned, I can\n\tconfidently call this \"visually lossless\", and it's definitely good enough\n\tfor regular watching and even single-frame stepping on this blog.\u003cbr\u003e\n\tSince the appeal of the original lossless files is undeniable though, I also\n\tmade those more easily available. You can directly download the one for the\n\tcurrently active video with the \u003cspan style=\"font-family:\n\tReC98 video player symbols;\"\u003e⍗\u003c/span\u003e button in the new video player – or \u003ca\n\thref=\"https://github.com/nmlgc/rec98.nmlgc.net/tree/master/blog/video/zmbv\"\u003edirectly\n\tget all of them from the Git repository\u003c/a\u003e if you don't like clicking.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tUnfortunately, even that only made up for half of the complexity in this\n\tpipeline. As impressive as the AV1 \u003ccode\u003e-crf 1\u003c/code\u003e result may be, it\n\tdoes in fact come with the drawback of also being impressively heavy to\n\tdecode within today's browsers. Seeking is dog slow, with even the latencies\n\tfor \u003ci\u003esingle-frame stepping\u003c/i\u003e being way beyond what I'd consider\n\ttolerable. To compensate, we have to invest another 78\u0026nbsp;MiB into turning\n\tevery 10\u003csup\u003eth\u003c/sup\u003e frame into a keyframe until single-stepping through an\n\tentire video becomes as fast as it could be on my system.\u003cbr\u003e\n\tBut fine, 146\u0026nbsp;MiB, that's still less than the 178\u0026nbsp;MiB that the old\n\tcommitted VP9 files used to take up. However, we still want to support VP9\n\tfor \u003ca href=\"https://caniuse.com/?search=av1\"\u003eolder browsers, older\n\thardware, and people who use Safari\u003c/a\u003e. And it's this codec where keyframes\n\tare so bad that there is no clear best solution, only compromises. The main\n\tissue: The lower you turn VP9's \u003ccode\u003e-crf\u003c/code\u003e value, the slower the\n\tseeking performance \u003ci\u003ewith the same number of keyframes\u003c/i\u003e. Conversely,\n\tthis means that raising quality also requires more keyframes for the same\n\tseeking performance – and at these file sizes, you really don't want to\n\traise either. We're talking 1.2\u0026nbsp;\u003ci\u003eGiB\u003c/i\u003e for all 86 videos at\n\t\u003ccode\u003e-crf 10\u003c/code\u003e and \u003ccode\u003e-g 5\u003c/code\u003e, and even on that configuration,\n\tseeking takes 1.3× as long as it would in the optimal case.\n\u003c/p\u003e\u003cp\u003e\n\tThankfully, a full VP9 encode of all 86 videos only takes some 30 minutes as\n\topposed to 9 hours. At that speed, it made sense to try a larger number of\n\tencoding settings during the ongoing development of the player. Here's a\n\ttable with all the trials I've kept:\n\u003c/p\u003e\u003cfigure\u003e\u003ctable id=\"trials-2022-10-31\" class=\"numbers\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eCodec\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003e-﻿crf\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003e-﻿g\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003cth style=\"text-align: left;\"\u003eOther parameters\u003c/th\u003e\n\t\t\t\u003cth\u003eTotal size\u003c/th\u003e\n\t\t\t\u003cth\u003eSeek time\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv420p\u003c/td\u003e\n\t\t\t\u003ctd\u003e111 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 10 -qmax 10 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e120 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e7\u003c/td\u003e\n\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 7 -qmax 7 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e140 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"av1 active\"\u003e\n\t\t\t\u003ctd\u003eAV1\u003c/td\u003e\n\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e146 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 10 -qmax 10 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e147 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8 active\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e6\u003c/td\u003e\n\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 6 -qmax 6 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e149 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e15\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 15 -qmax 15 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e177 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 10 -qmax 10 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e225 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e329 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e0-4\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 0 -qmax 4 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e376 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e32 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 5 -qmax 5 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e169 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e33 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e63\u003c/td\u003e\n\t\t\t\u003ctd\u003e40\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e47 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e34 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e146 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e34 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 0 -qmax 4 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e192 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e34 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003ctd\u003e40\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 4 -qmax 4 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e168 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e35 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e25\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e173 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e36 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e15\u003c/td\u003e\n\t\t\t\u003ctd\u003e15\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e252 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e36 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e25\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e118 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e37 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e190 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e37 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e19\u003c/td\u003e\n\t\t\t\u003ctd\u003e21\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e187 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e38 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e553 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e38 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-tune-content screen\u003c/td\u003e\n\t\t\t\u003ctd\u003e553 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e-tile-columns 6 -tile-rows 2\u003c/td\u003e\n\t\t\t\u003ctd\u003e553 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9 active\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e15\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv422p\u003c/td\u003e\n\t\t\t\u003ctd\u003e207 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e39 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e10\u003c/td\u003e\n\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e1210 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e43 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e264 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e45 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=yuv444p\u003c/td\u003e\n\t\t\t\u003ctd\u003e215 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e46 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e-vf format=gbrp10le\u003c/td\u003e\n\t\t\t\u003ctd\u003e272 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e49 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e63\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e24 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e67 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp8\"\u003e\n\t\t\t\u003ctd\u003eVP8\u003c/td\u003e\n\t\t\t\u003ctd\u003e0-4\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e-qmin 0 -qmax 4 -b:v 1G\u003c/td\u003e\n\t\t\t\u003ctd\u003e119 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e76 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr class=\"vp9\"\u003e\n\t\t\t\u003ctd\u003eVP9\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e107 MiB\u003c/td\u003e\n\t\t\t\u003ctd\u003e170 s\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cfigcaption\u003e\n\tThe \u003cstrong\u003ebold rows\u003c/strong\u003e correspond to the final encoding choices that\n\tare live right now. The seeking time was measured by holding →\u0026nbsp;Right on\n\tthe \u003ca href=\"/blog/2022-03-05\"\u003e📝 cheeto dodge strategy video\u003c/a\u003e.\n\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tYup, the compromise ended up including a chroma subsampling conversion to\n\tYUV422P. That's the one thing you \u003ci\u003edon't\u003c/i\u003e want to do for retro pixel\n\tgraphics, as it's the exact cause behind washed-out colors and red fringing\n\taround edges:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003crec98-child-switcher\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Subsampling-ZMBV.png?c289201a\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"Lossless\"\n\t\talt=\"The lossless source image\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Subsampling-VP9.png?6c938d9f\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"VP9 \u003ccode\u003e-crf 15 -vf format=yuv422p\u003c/code\u003e\"\n\t\talt=\"The same image encoded in VP9, exhibiting a severe case of chroma subsampling\"\n\t\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-10-31-Subsampling-VP9-difference.png?b9e68ec6\"\n\t\twidth=\"1280\"\n\t\tdata-title=\"Normalized difference\"\n\t\talt=\"The normalized difference between both images\"\n\t\tclass=\"active\"\n\t\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-child-switcher\u003e\n\t\u003cfigcaption\u003e\n\t\tThe worst example of chroma subsampling in a VP9-encoded file according\n\t\tto the above metric, from frame 130 (0-based) of\n\t\t\u003ca href=\"/blog/2022-01-31\"\u003e📝 Sariel's restored leaf \"spark\" animation\u003c/a\u003e,\n\t\tfeaturing smeared-out contours and even an all-around darker image,\n\t\tblowing up the image to a whopping 3653 colors. It's certainly an\n\t\taesthetic.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBut there simply was no satisfying solution around the ~200\u0026nbsp;MiB mark\n\twith RGB colors, and even this compromise is still a disappointment in both\n\tsize and seeking speed. Let's hope that \u003ca\n\thref=\"https://developer.apple.com/documentation/coremedia/kcmvideocodectype_av1/\"\u003eSafari\n\tusers do get AV1 support soon\u003c/a\u003e… Heck, even VP8, with its exclusive\n\tsupport for YUV420P, performs much better here, with the impact of\n\t\u003ccode\u003e-crf\u003c/code\u003e on seeking speed being much less pronounced. Encoding VP8\n\talso just takes 3 minutes for all 86 videos, so I could have experimented\n\tmuch more. Too bad that it only matters for \u003ci\u003ereally\u003c/i\u003e ancient systems…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tTwo final takeaways about VP9:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ccode\u003e-tune-content screen\u003c/code\u003e and the tile options make no\n\tdifference at all.\u003c/li\u003e\n\t\u003cli\u003eAll results used two-pass encoding. VP9 is the only codec where two\n\tpasses made a noticeable difference, cutting down the final encoded size\n\tfrom 224\u0026nbsp;MiB to 207\u0026nbsp;MiB. For AV1, compression even seems to be\n\tslightly worse with two passes, yielding 154,201,892 bytes rather than the\n\t153,643,316 bytes we get with a single pass. But that's a difference of\n\t0.36%, and hardly significant.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, \u003ci\u003enow\u003c/i\u003e we're done with codecs and get to finish the work on the\n\tpipeline with perhaps its biggest advantage. With a ffmpeg conversion\n\tinfrastructure in place, we can also easily output a video's first frame as\n\ta \u003ci\u003eposter\u003c/i\u003e image to be passed into the \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e tag.\n\tIf this image is kept at the exact resolution of the video, the browser\n\tdoesn't need to wait for an indeterminate amount of \"video metadata\" to be\n\tloaded, and can reserve the necessary space in the page layout much faster\n\tand without any of these dreaded loading spinners. For the big\n\t\u003ccode\u003e/blog\u003c/code\u003e page, this cuts down the minimum amount of required\n\tresources from 69.5 MB to 3.6 MB, finally making it usable again without\n\twaiting an eternity for the page to fully load. It's become pretty bad, so I\n\treally had to prioritize this task before adding any more blog posts on top.\n\u003c/p\u003e\u003cp\u003e\n\tThat leaves the player itself, which is basically a sum of lots of little\n\timplementation challenges. Single-frame stepping and seeking to discrete\n\tframes is the biggest one of them, as it's \u003ca\n\thref=\"https://github.com/w3c/media-and-entertainment/issues/4\"\u003e\u003ci\u003etechnically\u003c/i\u003e\n\tnot possible within the \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e tag\u003c/a\u003e, which only\n\treturns the current time as a continuous value in seconds. It only \u003ci\u003esort\n\tof\u003c/i\u003e works for us because the backend can pass the necessary FPS and frame\n\tcount values to the frontend. These allow us to place a discrete grid of\n\tframe \"frets\" at regular intervals, and thus establish a consistent mapping\n\tfrom frames to seconds and back. The only drawback here is a noticeably\n\tweird jump back by one frame when pausing a video within the second half of\n\ta frame, caused by snapping the continuous time in seconds back onto the\n\tframe grid in order to maintain a consistent frame counter. But the whole\n\tfeature of frame-based seeking more than makes up for that.\u003cbr\u003e\n\tThe new scrubbable timeline might be even nicer to use with a mouse or a\n\tfinger than just letting a video play regularly. With all the tuning work I\n\tput into keyframes, seeking is buttery smooth, and much better than the\n\tbuilt-in \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e UI of either Chrome or Firefox.\n\tUnfortunately, it still costs a whole lot of CPU, but I'd say it's worth it.\n\t🥲\n\u003c/p\u003e\u003cp\u003e\n\tFinally, the new player also has a few features that might not be\n\timmediately obvious:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eKeybindings for almost everything you might want them for, indicated by\n\thovering on top of each button. The tab switchers additionally support the\n\t↑\u0026nbsp;Up and ↓\u0026nbsp;Down keys to cycle through all tabs, or the number keys\n\tto jump to a specific tab. Couldn't find a way to indicate these mappings in\n\tthe UI yet.\u003c/li\u003e\n\t\u003cli\u003ePer-video captions now reserve the maximum height of any caption in the\n\tlayout. This prevents layout reflows when switching through such videos,\n\twhich previously caused quite annoying lag on the big \u003ccode\u003e/blog\u003c/code\u003e\n\tpage.\u003c/li\u003e\n\t\u003cli\u003eUseful fullscreen modes on both desktop and mobile, including all\n\tmarkers and the video caption. Firefox made this harder than it needed to\n\tbe, and if it weren't for \u003ccode\u003edisplay: contents\u003c/code\u003e, the implementation\n\twould have been even worse. In the end though, we didn't even need any video\n\tpixel sizes from the backend – just as it should be…\u003c/li\u003e\n\t\u003cli\u003e… and supporting Firefox was definitely worth it, as it's the only\n\tbrowser to support nearest-neighbor interpolation on videos.\u003c/li\u003e\n\t\u003cli\u003eAs some of the Unicode codepoints on the buttons aren't covered by the\n\tdefault fonts of some operating systems, I've taken them from the \u003ca\n\thref=\"https://catrinity-font.de/\"\u003eCatrinity font\u003c/a\u003e, licensed under the SIL\n\tOpen Font License. With \u003ca\n\thref=\"https://github.com/nmlgc/rec98.nmlgc.net/commit/3c2ffbf082149d7f86e0bc57a679e8547c04ff28\"\u003eall\n\tthe edits I did on this font\u003c/a\u003e, that license definitely was necessary. I\n\thope I applied it correctly though; it's not straightforward at all how to\n\tproperly license a \u003cq\u003eModified Version\u003c/q\u003e of an original font with a\n\t\u003cq\u003eReserved Font Name\u003c/q\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd with that, development hell is over, and I finally get to return to the\n\tcore business! Just more than one month late. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tNext up: Shipping the oldest still pending order, covering the TH04/TH05\n\tending script format. Meanwhile, the Seihou community also wants to keep\n\tinvesting in Shuusou Gyoku, so we're also going to see more of that on the\n\tside.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-09-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-10-31T23:58:39Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-09-04",
      "url": "https://rec98.nmlgc.net/blog/2022-09-04",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-10-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-09-04\"\u003e\u003ctime datetime=\"2022-09-04T15:13:44Z\"\u003e2022-09-04 15:13\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0217\"\u003eP0217\u003c/a\u003e\n\t\t\tSeihou / Shuusou Gyoku (Clean-room packfile implementation / Building a drop-in replacement binary)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ssg/compare/pbg...P0217\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/seihou\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A dormant shooting game series by ZUN\u0026#39;s former fellow students, who released the source code to the first two games in 2019.\"\u003eseihou\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sh01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"秋霜玉 / Shuusou Gyoku\"\u003esh01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tFirst of all: This blog is now available as a web feed, in three different\n\tformats!\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/feed.xml\"\u003e\u003ccode\u003e/blog/feed.xml\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/feed.atom\"\u003e\u003ccode\u003e/blog/feed.atom\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"/blog/feed.json\"\u003e\u003ccode\u003e/blog/feed.json\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThanks to \u003ca href=\"https://github.com/handlerug\"\u003ehandlerug\u003c/a\u003e for\n\timplementing and PR'ing the feature in a very clean way. That makes at least\n\ttwo people I know who wanted to see feed support, so there are probably\n\ta few more out there.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, Shuusou Gyoku. pbg released the original source code for the first two\n\tSeihou games back in February 2019, but notably removed the crucial\n\tdecompression code for the original packfiles due to… various unspecified\n\treasons, considerations, and implications. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e This vague\n\tlanguage and subsequent \u003ca\n\thref=\"https://github.com/pbghogehoge/ssg/pull/1\"\u003erejection of a pull request\n\tto add these features back in\u003c/a\u003e were probably the main reasons why no one\n\thas publicly done anything with this codebase since.\n\u003c/p\u003e\u003cp\u003e\n\tThe only other fork I know about is \u003ca\n\thref=\"https://github.com/Priw8\"\u003ePriw8\u003c/a\u003e's private fork from 2020, but only\n\tbecause \u003ca\n\thref=\"https://twitter.com/WishMakers_TH/status/1520633977756786688\"\u003eWishMakers\n\tinformed me about it\u003c/a\u003e shortly after this push was funded. Both of them\n\tmight also contribute some features to my fork in the future if their time\n\tallows it.\u003cbr\u003e\n\tIn this fork, Priw8 replaced packfile decompression with raw reads from\n\tdirectories with the pre-extracted contents of all the .DAT files. This\n\tworks for playing the game, but there are actually two more things that\n\trequire the original packfile code:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eHigh scores are stored as a bitstream with every variable separated by\n\tan alternating 0 or 1 bit, using the same bit-level access functions as the\n\tpackfile reader. That's a quite… unique form of obfuscation: It requires way\n\ttoo much code to read and write the format, and doesn't even obfuscate the\n\tdata \u003ci\u003ethat\u003c/i\u003e well because you can still see clear patterns when opening\n\tthese scorefiles in a hex editor.\u003c/li\u003e\n\t\u003cli\u003eReplays are 2-\"file\" archives compressed using the same algorithm as the\n\tpackfile. The first \"file\" contains metadata like the shot type, stage, and\n\tRNG seed, and the second one contains the input state for every frame.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tWe can surely implement our own simple and uncompressed formats for these\n\tthings, but it's not the best idea to build all future Shuusou Gyoku\n\tfeatures on top of a replay-incompatible fork. So, what do we do? On the one\n\thand, pbg expressed the clear wish to not include data reverse-engineered\n\tfrom the original binary. On the other hand, he released the code under the\n\tMIT license, which allows us to modify the code and distribute the results\n\tin any way we wish.\u003cbr\u003e\n\tSo, let's meet in the middle, and go for a clean-room implementation of the\n\tmissing features as indicated by their usage, without looking at either the\n\toriginal binary or wangqr's reverse-engineered code.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith incremental rebuilds being broken in the latest Visual Studio project\n\tfiles as well, it made sense to start from scratch on pbg's last commit. Of\n\tcourse, I can't pass up a chance to use\n\t\u003ca href=\"/blog/2020-09-03\"\u003e📝 Tup, my favorite build system\u003c/a\u003e for every\n\tproject I'm the main developer of. It might not fit Shuusou Gyoku as well as\n\tit fits ReC98, but let's see whether it would be reasonable at all…\n\u003c/p\u003e\u003cp\u003e\n\t… and it's actually not too bad! Modern Visual Studio makes this a bit\n\tharder than it should be with all the intermediate build artifacts you have\n\tto keep track of. In the end though, it's still only \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/5c163b6adf746289bf80e449752da888cea09a97/Tuprules.lua\"\u003e70\n\tlines of Lua to have a nice abstraction for both Debug and Release\n\tbuilds\u003c/a\u003e. With this layer underneath, the \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/5c163b6adf746289bf80e449752da888cea09a97/Tupfile.lua\"\u003eactual\n\tShuusou Gyoku-specific part\u003c/a\u003e can be expressed as succinctly as in any\n\tother modern build system, while still making every compiler flag explicit.\n\tIt might be slightly slower than a traditional \u003ccode\u003e.vcxproj\u003c/code\u003e build\n\tdue to \u003ca\n\thref=\"https://randomascii.wordpress.com/2014/03/22/make-vc-compiles-fast-through-parallel-compilation/\"\u003elaunching\n\tone \u003ccode\u003ecl.exe\u003c/code\u003e process per translation unit\u003c/a\u003e, but the result is\n\tway more reliable and trustworthy compared to anything that involves Visual\n\tStudio project files. This simplicity paves the way for expanding the build\n\tprocess to multiple steps, and doing all the static checking on translation\n\tstrings that I never got to do for thcrap-based patches. Heck, I might even\n\tcompile all future translations directly into the binary…\n\u003c/p\u003e\u003cp\u003e\n\tEvery C++ build system will invariably be hated by \u003ci\u003esomeone\u003c/i\u003e, so I'd\n\tsay that your goal should always be to simplify the actually important parts\n\tof your build enough to allow everyone else to easily adapt it to their\n\tfavorite system. This Tupfile definitely does a better job there than your\n\taverage \u003ccode\u003e.vcxproj\u003c/code\u003e file – but if you still want such a thing (or,\n\tgasp, 🤮\u0026nbsp;CMake project files\u0026nbsp;🤮) for better Visual Studio IDE\n\tintegration, you should have no problem generating them for yourself.\u003cbr\u003e\n\tThere might still be a point in doing that because that's the one part that\n\tunfortunately sucks about this approach. Visual Studio is horribly broken\n\tfor any nonstandard C++ project even in 2022:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eMakefile projects can be nicely integrated with Debug and Release\n\tconfigurations, but setting a later C++ language standard requires \u003ca\n\thref=\"https://github.com/Microsoft/VSLinux/issues/292#issuecomment-696271764\"\u003edumb\n\t\u003ccode\u003e.vcxproj\u003c/code\u003e hacks\u003c/a\u003e that don't even work properly anymore.\u003c/li\u003e\n\t\u003cli\u003eFolder projects are ridiculously ugly: The Build toolbar is permanently\n\tgrayed out \u003ci\u003eeven if you configured a build task\u003c/i\u003e. For some reason,\n\tconfiguring these tasks merely adds one additional element to a 9-element\n\tcontext menu in the Solution Explorer. Also, why does the big IDE use a\n\tdifferent JSON schema than the perfectly functional and adequate one from\n\tVisual Studio Code?\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn both cases, IntelliSense doesn't work properly \u003ci\u003eat all\u003c/i\u003e even if it\n\tappears to be configured correctly, and Tup's dependency tracking appeared\n\tto be weirdly cut off for the very final .PDB file. Interestingly though,\n\tusing the big Visual Studio IDE for just \u003ci\u003edebugging\u003c/i\u003e a binary via\n\t\u003ccode\u003edevenv bin/GIAN07.exe\u003c/code\u003e suddenly eliminates all the IntelliSense\n\tissues. Looks like there's a lot of essential information stored in the .PDB\n\tfiles that Visual Studio just refuses to read in any other context.\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tBut now compare that to Visual Studio Code: Open it from the \u003ci\u003ex64_x86\n\tCross Tools Command Prompt\u003c/i\u003e via \u003ccode\u003ecode .\u003c/code\u003e,  launch a build or\n\tdebug task, or browse the code with perfect IntelliSense. Three small\n\tconfiguration files and everything just works – heck, you even get the Tup\n\tprogress bar in the terminal. It might be Electron bloatware and horribly\n\tslow at times, but Visual Studio Code has long outperformed regular Visual\n\tStudio in terms of non-debug functionality.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOn to the compression algorithm then… and it's just textbook \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Storer%E2%80%93Szymanski\"\u003eLZSS\u003c/a\u003e,\n\twith 13 bits for the offset of a back-reference and 4 bits for its length?\n\tHardly a trade secret there. The hard parts there all come from unexpected\n\tinefficiencies in the bitstream format:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eEncoding back-references as offsets into an 8 KiB ring buffer dictionary\n\tmeans that the most straightforward implementation actually needs an 8 KiB\n\tarray for the LZSS sliding window. This could have easily been done with\n\tzero additional memory if the offset was encoded as the difference to the\n\tcurrent byte instead.\u003c/li\u003e\n\t\u003cli\u003eThe packfile format stores the uncompressed size of every file in its\n\theader, which is a good thing because you want to know in advance how much\n\theap memory to allocate for a specific file. Nevertheless, the original game\n\tonly stops reading bits from the packfile once it encountered a\n\tback-reference with an offset of 0. This means that the compressor not only\n\thas to write this technically unneeded back-reference to the end of the\n\tcompressed bitstream, but also ignore any potential other longest\n\tback-reference with an offset of 0 \u003ci\u003ewithin\u003c/i\u003e the file. The latter can\n\teasily happen with a ring buffer dictionary.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tThe original game used a single \u003ccode\u003eBIT_DEVICE\u003c/code\u003e class with mode\n\tflags for every combination of reading and writing memory buffers and\n\ton-disk files. Since that would have necessitated a lot of error checking\n\tfor all (pseudo-)methods of this class, I wrote one dedicated small class\n\tfor each one of these permutations instead. To further emphasize the\n\tclean-room property of this code, these use modern C++ memory ownership\n\tfeatures: \u003ccode\u003estd::unique_ptr\u003c/code\u003e for the fixed-size read-only buffers\n\twe get from packfiles, \u003ccode\u003estd::vector\u003c/code\u003e for the newly compressed\n\tbuffers where we don't know the size in advance, and \u003ccode\u003estd::span\u003c/code\u003e\n\tfor a borrowed reference to an immutable region of memory that we want to\n\ttreat as a bitstream. Definitely better than using the native Win32\n\t\u003ccode\u003eLocalAlloc()\u003c/code\u003e and \u003ccode\u003eLocalFree()\u003c/code\u003e allocator, especially\n\tif we want to port the game away from Windows one day.\n\u003c/p\u003e\u003cp\u003e\n\tOne feature I didn't use though: C++ fstreams, because those are trash.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e These days, they would seem to be the natural\n\tchoice with the new \u003ccode\u003estd::filesystem::path\u003c/code\u003e type from C++17:\n\tCorrectly constructed, you can pass that type to an fstream constructor and\n\tgain both locale independence on Windows \u003ci\u003eand\u003c/i\u003e portability to\n\teverything else, without writing any Windows-specific UTF-16 code. But even\n\tin a Release build, fstreams add ~100 KB of locale-related bloat to the .EXE\n\twhich adds no value for just reading binary files. That's just too\n\tembarrassing if you look at how much space the rest of the game takes up.\n\tWriting your own platform layer that calls the Win32\n\t\u003ccode\u003eCreateFileW()\u003c/code\u003e, \u003ccode\u003eReadFile()\u003c/code\u003e, and\n\t\u003ccode\u003eWriteFile()\u003c/code\u003e API functions is apparently still the way to go\n\teven in 2022. And with \u003ccode\u003estd::filesystem::path\u003c/code\u003e still being a\n\twelcome addition to C++, it's not too much code to write either.\n\u003c/p\u003e\u003cp\u003e\n\tThis gets us file format compatibility with the original release… and a\n\tcrash as soon as the ending starts, but only in Release mode? As it turns\n\tout, this crash is caused by \u003ca\n\thref=\"https://github.com/nmlgc/ssg/commit/5c163b6adf746289bf80e449752da888cea09a97\"\u003ean\n\tout-of-bounds\u003c/a\u003e \u003ca\n\thref=\"https://github.com/nmlgc/ssg/blob/5c163b6adf746289bf80e449752da888cea09a97/GIAN07/ENDING.CPP#L127-L128\"\u003earray\n\taccess bug\u003c/a\u003e that was present even in the original game, and only turned\n\tinto a crash now because the optimizer in modern Visual Studio versions\n\treorders static data. As a result, the 6-element \u003ccode\u003epFontInfo\u003c/code\u003e\n\tarray got placed in front of an ECL-related counter variable that then got\n\tcorrupted by the write to the 7\u003csup\u003eth\u003c/sup\u003e element, which  subsequently\n\tcrashed the game with a read access to previously deallocated danmaku script\n\tdata. That just goes to show that these \u003ci\u003etechnical\u003c/i\u003e bugs are important\n\tand worth fixing even if they don't cause issues in the original game. Who\n\tknows how many of these will turn into crashes once we get to porting PC-98\n\tTouhou?\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo here we go, a new build of Shuusou Gyoku, compiled with Visual Studio\n\t2022, and compatible with all original data formats:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca class=\"release\" href=\"https://github.com/nmlgc/ssg/releases/tag/P0217\"\u003e\n\t\u003cimg src=\"/static/emoji-sh01.png?4b49dc8b\" alt=\":sh01:\" width=\"24\" height=\"24\" \u003e Shuusou Gyoku P0217\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tInside the regular Shuusou Gyoku installation directory, this binary works\n\tas a full-fledged drop-in replacement for the original\n\t\u003ccode\u003e秋霜玉.exe\u003c/code\u003e. It still has all of the original binary's problems\n\tthough:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSeparate Japanese locale emulation is still needed to correctly refer to\n\tthe original names of the configuration (\u003ccode\u003e秋霜CFG.DAT\u003c/code\u003e), score\n\t(\u003ccode\u003e秋霜SC.DAT\u003c/code\u003e), and replay (\u003ccode\u003e秋霜りぷ*.DAT\u003c/code\u003e) files.\n\tIt's also required for the ending text to not render as mojibake.\u003c/li\u003e\n\t\u003cli\u003eRunning the game at full speed and without graphical glitches on modern\n\tWindows still requires a separate DirectDraw patch such as \u003ca\n\thref=\"https://github.com/narzoul/DDrawCompat/releases\"\u003eDDrawCompat\u003c/a\u003e. To\n\teliminate any remaining flickering, configure the game to use 16-bit\n\tgraphics in the \u003ci\u003eConfig → Graphic\u003c/i\u003e menu.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs well as some of its own:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe original screenshot feature is still missing, as it also wasn't part\n\tof pbg's released source code.\u003c/li\u003e\n\t\u003cli\u003eIf you're wondering why your antivirus is freaking out: I explained the\n\treasons in \u003ca href=\"https://github.com/nmlgc/ssg/issues/22\"\u003ethis GitHub\n\tissue\u003c/a\u003e, with \u003ca href=\"https://github.com/nmlgc/ssg/issues/21\"\u003esome more\n\tbackground here\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo all in all, it's a strict downgrade at this point in time.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e And more of a symbol that we can now start\n\tdoing actual work on this game. Seihou has been a fun change of pace, and I\n\thope that I get to do more work on the series. There is quite a lot to be\n\tdone with Shuusou Gyoku alone, and the \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues\"\u003e21 GitHub issues\u003c/a\u003e I've opened\n\tare probably only scratching the surface.\u003cbr\u003e\n\tHowever, all the required research for this one consumed more like 1⅔\n\tpushes. Despite just one push being funded, it wouldn't have made sense to\n\trelease the commits or this binary in any earlier state. To repay this debt,\n\tI'm going to put the next \u003cscript\u003eformatCurrency(5000)\u003c/script\u003e\u003cnoscript\u003e50.00\u0026nbsp;€\u003c/noscript\u003e for Seihou towards the\n\tsmall code maintenance and performance tasks that I usually do for free,\n\tbefore doing any more feature and bugfix work. Next up: Improving video\n\tplayback on the blog, and maybe delivering some microtransaction work on the\n\tside?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-10-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-09-04T15:13:44Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-08-15",
      "url": "https://rec98.nmlgc.net/blog/2022-08-15",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-09-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-08-15\"\u003e\u003ctime datetime=\"2022-08-15T23:57:46Z\"\u003e2022-08-15 23:57\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0216\"\u003eP0216\u003c/a\u003e\n\t\t\tTH01 decompilation (100%)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3123c9d...a0ff3f1\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eJonathKane\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tOn August 15, 1997, at Comiket 52, an unknown doujin developer going by the\n\tname of ZUN released his first game, \u003ccite\u003e\u003cspan lang=\"ja\"\u003e東方靈異伝　～\n\t\u003c/span\u003e The Highly Responsive to Prayers\u003c/cite\u003e, marking the start of the\n\tTouhou Project game series that keeps running to this day. Today, exactly 25\n\tyears later, the C++ source code to version 1.10 of that game has been\n\tcompletely and perfectly reconstructed, reviewed, and documented.\n\u003c/p\u003e\u003cfigure class=\"fullres pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2022-08-15-TH01-Title.png?fbadcf01\" alt=\"The TH01 title image.\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd with that, a warm welcome to all game journalists who have\n\t(re-)discovered this project through these news! Here's a summary for\n\teveryone who doesn't want to go through 3 years worth of blog posts:\n\u003c/p\u003e\u003ch5\u003eWhat does this mean?\u003c/h5\u003e\u003cul\u003e\n\t\u003cli\u003eAll code that ZUN wrote as part of a TH01 installation has now been\n\tdecompiled to C++ code. The only parts left in assembly are two third-party\n\tlibraries (master.lib and PiLoad), which were originally written in\n\tassembly, and are built from their respective official source code.\u003c/li\u003e\n\t\u003cli\u003eYou can clone the \u003ca href=\"https://github.com/nmlgc/ReC98\"\u003eReC98\n\trepository\u003c/a\u003e, set up the build environment, and get a binary with an\n\tidentical program image. The hashes of the resulting executables won't match\n\tthose of ZUN's original release, but all differences there stem from details\n\tin the .EXE header that don't influence program execution, such as the\n\ton-disk order of the conceptually unordered set of x86 memory segment\n\trelocations. If you're interested in that level of correctness, you can\n\torder \u003ci\u003eEasier verification against original binaries\u003c/i\u003e from the store.\n\tFor now though, use \u003ca href=\"https://github.com/nmlgc/mzdiff\"\u003emzdiff\u003c/a\u003e for\n\tverifying the builds against ZUN's binaries.\u003c/li\u003e\n\t\u003cli\u003eEver since this crowdfunding has started 3 years ago, the goal of this\n\tproject has shifted more and more towards a full-on code review rather than\n\tbeing just a mechanical decompilation:\u003cul\u003e\n\t\t\u003cli\u003eHardcoded constants were derived from as few truly hardcoded values\n\t\tas possible, which uncovered their intended meaning and highlighted any\n\t\tinconsistencies\u003c/li\u003e\n\t\t\u003cli\u003eCode was deduplicated to a perhaps obsessive level (I'm still trying\n\t\tto find a balance)\u003c/li\u003e\n\t\t\u003cli\u003eTons of comments everywhere to put everything into context\u003c/li\u003e\n\t\t\u003cli\u003eAnd, of course, \u003ca href=\"/blog/tag/th01\"\u003e2½ years worth of blog\n\t\tposts\u003c/a\u003e summarizing any highlights, glitches, and secrets. (There\n\t\tmight still be some left to be discovered!)\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eAs a result, modding the games and porting them away from the PC-98\n\tplatform is now a lot easier.\u003c/li\u003e\n\u003c/ul\u003e\u003ch5\u003eWhat does this not mean?\u003c/h5\u003e\u003cul\u003e\n\t\u003cli\u003eThis is not a piracy release. ReC98 only provides the code that the\n\tgame's .EXE and .COM files are built out of. Without the rest of the\n\toriginal data files, supplied from a pre-existing game copy, the code won't\n\tdo very much.\u003c/li\u003e\n\t\u003cli\u003eEven apart from ZUN's own code quality, the ReC98 repository is not as\n\tpolished and consistent as it could be, having seen multiple code structure\n\tevolutions over the 8 years of its existence.\u003c/li\u003e\n\t\u003cli\u003eTH01 hasn't magically reached Doom levels of easy portability now. As a\n\tdecompilation of the exact code that ZUN wrote for the PC-98 platform, it is\n\t\u003ci\u003every\u003c/i\u003e PC-98-native, and wildly mixes game logic with hardware\n\taccesses. As ZUN's first foray into game development, he understandably\n\tdidn't see the need for writing an engine or hardware abstraction layer\n\tyet.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo while this milestone opened the floodgates to PC-98-native mods, I\n\twouldn't advise trying to attempt a port away from PC-98 right now. But then\n\tagain, I have a financial interest in being a part of the porting process,\n\tand who knows, maybe you \u003ci\u003ecan\u003c/i\u003e just merge in a PC-98 emulator core and\n\tget started with something halfway decent in a short amount of time. \u003cs\u003eAfter\n\tall, TH01 is by far the easiest PC-98 Touhou game to port to other systems,\n\tas it makes the least use of hardware features.\u003c/s\u003e (\u003cstrong\u003eEdit\n\t(2023-03-30)\u003c/strong\u003e: \u003ca href=\"/blog/2023-03-30\"\u003e📝 Turns out\u003c/a\u003e that this\n\tcrown actually goes to TH02. It features the least amount of ZUN-written\n\tPC-98-specific rendering code out of all the 5 games, with most of it\n\tbeing decently abstracted via master.lib.)\n\u003c/p\u003e\u003cp\u003e\n\tHowever, this game in particular raises the question of what \u003ci\u003eexactly\u003c/i\u003e\n\tone would even \u003ci\u003ewant\u003c/i\u003e to port. TH01 is a broken flicker-fest that\n\toverwhelmingly suffers the drawbacks of PC-98 hardware rather than using it\n\tto its advantage. Out of the 78 bugs that I ended up labeling as such, the\n\tmajority are \u003ca href=\"/blog/tag/th01/blitting\"\u003esprite blitting issues\u003c/a\u003e,\n\twhile you can  \u003ca href=\"/blog/tag/th01/good-code\"\u003ecount the instances of\n\tgood hardware use on one hand\u003c/a\u003e.\u003cbr\u003e\n\tAnd even at the level of game logic, this game features a \u003ci\u003elot\u003c/i\u003e of\n\tweird, inconsistent behavior. Less rigorous projects such as \u003ca\n\thref=\"https://github.com/Wintiger0222/uth05win\"\u003euth05win\u003c/a\u003e would probably\n\tpromptly identify these issues as bugs and fix them. On the one hand, this\n\tshows that there is a part of the community that wants sane versions of\n\tthese games which behave as expected. In other parts of the community\n\tthough, such projects quickly gain the reputation of being too inaccurate to\n\tbother about them.\n\u003c/p\u003e\u003cp\u003e\n\tSome terminology might help here. If you look over the ReC98 codebase,\n\tyou'll find that I classified any weird code into three categories.\n\t\u003cstrong\u003eEdit (2023-03-05):\u003c/strong\u003e These have been overhauled with a new\n\t\u003cq\u003elandmine\u003c/q\u003e category for invisible issues. Check \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/blob/master/CONTRIBUTING.md#labeling-weird-or-broken-code\"\u003e\u003ccode\u003eCONTRIBUTING.md\u003c/code\u003e\u003c/a\u003e\n\tfor the complete and current current definition of all weird code\n\tcategories.\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli id=\"zun-bug\"\u003e\u003ca href=\"#zun-bug\"\u003e🔗 \u003cstrong\u003eZUN bugs\u003c/strong\u003e\u003c/a\u003e: Broken\n\tcode that results from logic errors or incorrect programming\n\tlanguage/API/hardware use, with enough evidence in the code to indicate that\n\tZUN did not intend the bug. Fixing these issues must not affect hypothetical\n\treplay compatibility, and any resulting visual changes must match ZUN's\n\tprovable intentions.\u003c/li\u003e\n\t\u003cli id=\"zun-quirk\"\u003e\u003ca href=\"#zun-quirk\"\u003e🔗 \u003cstrong\u003eZUN quirks\u003c/strong\u003e\u003c/a\u003e:\n\tWeird code that looks incorrect in context. Fixing these issues would change\n\tgameplay enough to desync a hypothetical replay recorded on the original\n\tversion, or affect the visuals so much that the result is no longer faithful\n\tto ZUN's original release. It might very well be called a fangame at that\n\tpoint.\u003c/li\u003e\n\t\u003cli id=\"zun-bloat\"\u003e\u003ca href=\"#zun-bloat\"\u003e🔗 \u003cstrong\u003eZUN bloat\u003c/strong\u003e\u003c/a\u003e:\n\tCode that wastes memory, CPU cycles, or even just the mental capacity of\n\tanyone trying to read and understand the code. If you want to write a\n\tparticularly resource-intensive mod, these are the places you could claim\n\tsome of those resources from.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSome examples:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAll crashes are bugs\u003c/li\u003e\n\t\u003cli\u003eAll blitting issues related to inappropriate VRAM byte alignment are\n\tbugs\u003c/li\u003e\n\t\u003cli\u003eAll the \u003ca href=\"/blog/2022-08-08\"\u003e📝 incorrect coordinate calculations in the YuugenMagan fight\u003c/a\u003e are quirks\u003c/li\u003e\n\t\u003cli\u003eThe overly high damage of TH10's MarisaB 3.00-3.95 power shot is a\n\tquirk, \u003ca href=\"https://nylilsa.github.io/#/bugs/th10/0\"\u003edespite having been\n\tproven to be a typo\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eThe idea of splitting TH01 across three executables is its biggest\n\tsource of bloat. It wastes disk space, the game doesn't even make use of the\n\tmemory gained from unloading unneeded code and data, it complicates the\n\tbuild process and code structure with inconsistencies between the individual\n\tbinaries, and the required inter-process communication via shared memory\n\tadds another piece of global state mutation headache.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSince I'm not in the business of writing fanfiction, I won't offer any\n\toption that fixes \u003ci\u003equirks\u003c/i\u003e. That's where all of you can come in, and\n\tuse ReC98 as a base for remasters and remakes. As for bloat and bugs though,\n\tthere are many ways we could go from here:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIf you want to ultimately try porting the game yourself, but still\n\tsupport ReC98 somehow, I can recommend the \u003ci\u003eZUN code cleanup\u003c/i\u003e goal.\n\tThis is the most conservative option that leaves all bugs and quirks in\n\tplace and only removes \u003ci\u003ebloat\u003c/i\u003e, rearchitecting the codebase so that\n\tit's easier to work with.\u003c/li\u003e\n\t\u003cli\u003eFor an improved gameplay experience on PC-98, choose the \u003ci\u003eTH01\n\tAnniversary Edition\u003c/i\u003e goal. In addition to the above code cleanup, this\n\tgoal fixes every \u003ci\u003ebug\u003c/i\u003e with the game, most notably all the sprite\n\tflickering by implementing a completely new renderer, while maintaining\n\thypothetical replay compatibility to ZUN's original release.\u003c/li\u003e\n\t\u003cli\u003eIf you're mainly interested in seeing any variety of TH01 ported away\n\tfrom PC-98 to any system, choose the \u003ci\u003ePortability to non-PC-98 systems\u003c/i\u003e\n\tgoal. In this one, I'm going to develop the abstraction layers that would\n\tultimately bring this game to the aforementioned Doom level of portability,\n\twhile still keeping it running with better than original performance on\n\tPC-98.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eReplay support\u003c/i\u003e is also something you could order…\u003c/li\u003e\n\t\u003cli\u003e… as is \u003ci\u003eMultilingual translation support (on PC-98)\u003c/i\u003e, for those\n\tsweet non-ASCII characters if that's your thing.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThen again, with all these choices in mind, maybe we should just let TH01 be\n\twhat it is: ZUN's first game, evidence for the truth that no programmer\n\twrites good code the first time around, and more of a historical curiosity\n\tthan anything you'd want to maintain and modernize. The idea of moving on to\n\tthe next game and decompiling all 5 PC-98 Touhou games in order has\n\tcertainly \u003ca\n\thref=\"https://twitter.com/TheArandui/status/1549480991806324736\"\u003eshown to be\n\tpopular among the backers who funded this 100% goal\u003c/a\u003e.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSince the beginning of the year, I've been dramatically raising the level of\n\tquality and care I've been putting into this project, leading to 9 of the 10\n\tlongest blog posts having been written in the past 8 months. The community\n\treception has been even more supportive as well, with all of you still\n\tregularly selling out the store in return. To match the level of quality\n\twith the community demand, I'm raising push prices from\n\t\u003cscript\u003eformatCurrency(6000)\u003c/script\u003e\u003cnoscript\u003e60.00\u0026nbsp;€\u003c/noscript\u003e to \u003cscript\u003eformatCurrency(7500)\u003c/script\u003e\u003cnoscript\u003e75.00\u0026nbsp;€\u003c/noscript\u003e per push, as of this blog\n\tpost. \u003ca href=\"/blog/2021-12-01\"\u003e📝 As usual\u003c/a\u003e, I'm going to deliver any\n\texisting orders in the backlog at the value they were originally purchased\n\tat. Due to the way the cap has to be calculated, these contributions now\n\tappear to have increased in value by 25%.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, I do realize that this might make regular pushes prohibitively\n\texpensive for some. This could especially prevent all these exciting modding\n\tgoals from ever getting off the ground. Thinking about it though, the push\n\tsystem is only really necessary for the core reverse-engineering business,\n\twhere longer, concentrated stretches of work allow me to study a new piece\n\tof code in a larger context and improve the quality of the final result. In\n\tcontrast, modding-related goals could theoretically be segmented into\n\tarbitrarily small portions of work, as I have a clear idea of where I want\n\tto go and how to get there.\u003cbr\u003e\n\tThus, I'm introducing \u003ci\u003emicrotransactions\u003c/i\u003e, now available for all\n\tmodding-related goals. These allow you to order fractional pieces of work\n\tfor as low as 1 €, which I will immediately deliver without requiring others\n\tto fund a full push first. \u003cstrong\u003eEdit (2022-08-16):\u003c/strong\u003e And then the\n\tstore still sold out with a single regular contribution by\n\tnrook towards more reverse-engineering. Guess that this\n\texperiment will have to wait a little while longer, then… 😅\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Taking a break and recovering from crunch time by improving video\n\tplayback on this blog and \u003ca\n\thref=\"https://github.com/nmlgc/ssg/issues\"\u003eworking on Shuusou Gyoku\u003c/a\u003e,\n\tbefore returning to Touhou in September.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-09-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-08-15T23:57:46Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-08-14",
      "url": "https://rec98.nmlgc.net/blog/2022-08-14",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-08-14\"\u003e\u003ctime datetime=\"2022-08-14T23:17:23Z\"\u003e2022-08-14 23:17\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0214\"\u003eP0214\u003c/a\u003e\n\t\t\tTH01 decompilation (Orb and Game Over animations + Pause, continue, and debug menus)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/158a91e...414770c\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0215\"\u003eP0215\u003c/a\u003e\n\t\t\tTH01 decompilation (REIIDEN.EXE main() function / 100%)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/414770c...3123c9d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/card-flipping\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Debug features that ZUN left in the shipped game.\"\u003edebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tLast blog post before the 100% completion of TH01! The final parts of\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e would feel rather out of place in a celebratory\n\tblog post, after all. They provided quite a neat summary of the typical\n\ttechnical details that are wrong with this game, and that I now get to\n\tmention for one final time:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe Orb's animation cycle is maybe two frames shorter than it should\n\thave been, showing its last sprite for just 1 frame rather than 3: \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAKCgoPDw8AAAADLNMiH/C05FVFNDQVBFMi4wAwEAAAAh+QQFHgADACwAAAAAIAAgAEAChZyPqcsG/4IMQswb2hCg+u9hGKCBIfVBpMaoE6s8XGeJttSYuqdCcJJ6/IZEluuG+/UAlwqyIavQnEjMYreLTHwMrK439M6ExbL5jE4fIDfzsZqBLSefNnQOv7Rk0Vo+CTQ1dQJ3NYh1QwYkBuLCpcDYCJaj08Sj8mNiObZSlKWoFip6UAAAIfkEBR4AAwAsAwAEABkAGQAAAlucf6Eb6D+YVNDNSyvecHvrfUnIGSQjpIICmKcKB+0ZwDYASOtk9/mi0vVqNQFgV0QqesgYUDhcBjfMldXJq/YGKagWZviKU4exFmGuOtI2CJtcYVfQ37l7WykAACH5BAUeAAMALAMABAAZABkAAAJrnH+hwIAPhwqtuogmtRbL2WhcJFYLI6Tpo22MqMZZyaGxkLT1fRvt2eD1fkCbUEVcHIVJxZLXdD6RUemzCrpGLcvPbyf0gUDgnnhzSlV4M4ALEAuaE26uYC2D1Dj5yF6+4mGwpyKoZ4SDUQAAIfkEBQoAAwAsAwAEABkAGQAAAnGcf6Ar6D+WZNDNC4SuEesPQtgFbsjolSealaHBujI8ysF9b6iL94G2CACEipLPJwjeGCBkL6nAZVoaZDVHFAqTzedVCqJ+lxnp8CcTHEeDHM/HbrvHZszhuI4b8D2WI88CAEEWqMBRuMCR4KdoccFRAAAh/hVNYWRlIHdpdGggU2NyZWVuVG9HaWYAOw==\"\n\talt=\"TH01's Orb animation cycle as an animated GIF, with frame durations\n\tproportional to the ones found in the game\"\u003e\u003c/li\u003e\n\t\u003cli\u003eThe text in the Pause and Continue menus is not \u003ci\u003equite\u003c/i\u003e correctly\n\tcentered.\u003c/li\u003e\n\t\u003cli\u003eThe memory info screen hides quite a bit of information about the .PTN\n\tbuffers, and obscures even the info that it \u003ci\u003edoes\u003c/i\u003e show behind\n\tmisleading labels. The most vital information would have been that ZUN could\n\thave easily saved 20% of the memory by using a structure without the\n\tunneeded alpha plane… Oh, and the \u003cspan lang=\"ja\"\u003eREWIRTE\u003c/span\u003e option\n\tmapped to the ⬇️ down arrow key simply redraws the info screen. Might be\n\tuseful after a \u003cspan lang=\"ja\"\u003eNODE CHEAK\u003c/span\u003e, which replaces the output\n\twith its own, but stays within the same input loop.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut hey, there's an error message if you start \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e\n\twithout a resident MDRV2 or a correctly prepared resident structure! And\n\teven a good, user-friendly one, asking the user to launch the batch file\n\tinstead. For some reason, this convenience went out of fashion in the later\n\tgames.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe Game Over animation (how fitting) gives us TH01's final piece of weird\n\tsprite blitting code, which seriously manages to include 2 bugs and 3 quirks\n\tin under 50 lines of code. In test mode (\u003ccode\u003egame t\u003c/code\u003e or \u003ccode\u003egame\n\td\u003c/code\u003e), you can trigger this effect by pressing the ⬇️ down arrow key,\n\twhich certainly explains why I encountered seemingly random Game Over events\n\tduring all the tests I did with this game…\u003cbr\u003e\n\tThe animation appears to have changed quite a bit during development, to the\n\tpoint that probably even ZUN himself didn't know what he wanted it to look\n\tlike in the end:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThe original version unblits a 32×32 rectangle around Reimu that only\n\t\tgrows on the X axis… for the first 5 frames. The unblitting call is\n\t\tonly run if the corresponding sprite wasn't clipped at the edges of the\n\t\tplayfield in the frame before, and ZUN uses the \u003ci\u003eanimation's frame\n\t\tnumber\u003c/i\u003e rather than the sprite loop variable to index the per-sprite\n\t\tclip flag array. The resulting out-of-bounds access then reads the\n\t\tsprite \u003ci\u003ecoordinates\u003c/i\u003e instead, which are never 0, thus interpreting\n\t\tall 5 sprites as clipped.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThis variant would interpret the declared 5 effect coordinates as\n\t\tdistinct sprites and unblit them correctly every frame. The end result\n\t\tis rather wimpy though… hardly appropriate for a Game Over, especially\n\t\twith the original animation in mind.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThis variant would not unblit anything, and is probably closest to what\n\t\tthe final animation should have been.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-14-TH01-Gameover-original.webp?5ff07be9\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"22\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-original.avi?5c7245a0\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-14-TH01-Gameover-original.webm?fd26831d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-14-TH01-Gameover-original.webm?1aae347f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-14-TH01-Gameover-original.webm?9a6b9f15\" type=\"video/webm\"\u003eVideo of TH01's original Game Over animation, highlighting very quirky unblitting choices. \u003ca href=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-original.avi?5c7245a0\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-14-TH01-Gameover-wimpy.webp?5ff07be9\" preload=\"none\" controls data-title=\"\u0026quot;Proper\u0026quot; effect sprite unblitting\" loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"22\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-wimpy.avi?88749d31\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-14-TH01-Gameover-wimpy.webm?6fb85bc8\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-14-TH01-Gameover-wimpy.webm?ba1e1d92\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-14-TH01-Gameover-wimpy.webm?0638bde1\" type=\"video/webm\"\u003eVideo of TH01's Game Over animation with \"proper\" per-effect unblitting code, yielding a rather wimpy result. \u003ca href=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-wimpy.avi?88749d31\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-14-TH01-Gameover-no-unblitting.webp?5ff07be9\" preload=\"none\" controls data-title=\"No unblitting\" loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"22\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-no-unblitting.avi?3d135857\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-14-TH01-Gameover-no-unblitting.webm?17d6caee\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-14-TH01-Gameover-no-unblitting.webm?ef01dbbc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-14-TH01-Gameover-no-unblitting.webm?ddcfa217\" type=\"video/webm\"\u003eVideo of TH01's Game Over animation with no sprite unblitting whatsoever. \u003ca href=\"/blog/static/video/zmbv/2022-08-14-TH01-Gameover-no-unblitting.avi?3d135857\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tFinally, we get to the big \u003ccode\u003emain()\u003c/code\u003e function, serving as the duct\n\ttape that holds this game together. It may read rather disorganized with all\n\tthe (actually necessary) assignments and function calls, but the only\n\t\u003ci\u003eactual\u003c/i\u003e minor issue I've seen there is that you're robbed of any\n\tpellet destroy bonus collected on the final frame of the final boss. There\n\tis a certain charm in directly nesting the infinite main gameplay loop\n\twithin the infinite per-life loop within the infinite stage loop. But come\n\ton, why is there no fourth scene loop? \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Instead, the\n\tgame just starts a new \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e process before and after a\n\tboss fight. With all the wildly mutated global state, that was probably a\n\tmuch saner choice.\n\u003c/p\u003e\u003cp\u003e\n\tThe final secrets can be found in the debug stage selection. ZUN\n\timplemented the prompts using the C standard library's \u003ccode\u003escanf()\u003c/code\u003e\n\tfunction, which is the natural choice for quick-and-dirty testing features\n\tlike this one. However, the C standard library is also complete and utter\n\ttrash, and so it's not surprising that both of the \u003ccode\u003escanf()\u003c/code\u003e\n\tcalls do… well, probably not what ZUN intended. The guaranteed out-of-bounds\n\tmemory access in the \u003ccode\u003eselect_flag\u003c/code\u003e route prompt thankfully has no\n\treal effect on the game, but it gets really interesting with the \u003ccode\n\tlang=\"ja\"\u003e面数 \u003c/code\u003e stage prompt.\u003cbr\u003e\n\tBack in 2020, I already wrote about\n\t\u003ca href=\"/blog/2020-11-30\"\u003e📝 stages 21-24, and how they're loaded from actual data that ZUN shipped with the game\u003c/a\u003e.\n\tAs it now turns out, the code that maps stage IDs to \u003ccode\u003eSTAGE?.DAT\u003c/code\u003e\n\tscene numbers contains an explicit branch that maps any (1-based) stage\n\tnumber ≥21 to scene 7. Does this mean that an Extra Stage was indeed planned\n\tat some point? That branch seems way too specific to just be meant as a\n\tfallback. \u003ca href=\"https://www.youtube.com/watch?v=RcjhM4tfPq4\u0026t=169s\"\u003eMaybe\n\tAsprey was on to something after all…\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tHowever, since ZUN passed the stage ID as a signed integer to\n\t\u003ccode\u003escanf()\u003c/code\u003e, you can also enter negative numbers. The only place\n\tthat kind of accidentally checks for them is the aforementioned stage\n\tID\u0026nbsp;→\u0026nbsp;scene mapping, which ensures that (1-based) stages \u0026lt; 5 use\n\tthe shrine's background image and BGM. With no checks anywhere else, we get\n\ta new set of \"glitch stages\":\n\u003c/p\u003e\u003cfigure class=\"side_by_side medium\"\u003e\u003cfigure\u003e\n\t\t\u003ca href=\"/blog/static/2022-08-14-TH01-Stage-minus-1.png?dd3eb2ee\"\u003e\u003cimg src=\"/blog/static/2022-08-14-TH01-Stage-minus-1.png?dd3eb2ee\" alt=\"TH01's stage -1\"\u003e\u003c/a\u003e\n\t\t\u003cfigcaption\u003eStage -1\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure\u003e\n\t\t\u003ca href=\"/blog/static/2022-08-14-TH01-Stage-minus-2.png?66629edd\"\u003e\u003cimg src=\"/blog/static/2022-08-14-TH01-Stage-minus-2.png?66629edd\" alt=\"TH01's stage -2\"\u003e\u003c/a\u003e\n\t\t\u003cfigcaption\u003eStage -2\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure\u003e\n\t\t\u003ca href=\"/blog/static/2022-08-14-TH01-Stage-minus-3.png?4f87b63a\"\u003e\u003cimg src=\"/blog/static/2022-08-14-TH01-Stage-minus-3.png?4f87b63a\" alt=\"TH01's stage -3\"\u003e\u003c/a\u003e\n\t\t\u003cfigcaption\u003eStage -3\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure\u003e\n\t\t\u003ca href=\"/blog/static/2022-08-14-TH01-Stage-minus-4.png?77b301f8\"\u003e\u003cimg src=\"/blog/static/2022-08-14-TH01-Stage-minus-4.png?77b301f8\" alt=\"TH01's stage -4\"\u003e\u003c/a\u003e\n\t\t\u003cfigcaption\u003eStage -4\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure\u003e\n\t\t\u003ca href=\"/blog/static/2022-08-14-TH01-Stage-minus-5.png?9ac10d1d\"\u003e\u003cimg src=\"/blog/static/2022-08-14-TH01-Stage-minus-5.png?9ac10d1d\" alt=\"TH01's stage -5\"\u003e\u003c/a\u003e\n\t\t\u003cfigcaption\u003eStage -5\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe scene loading function takes the entered 0-based stage ID value modulo\n\t5, so these 4 are the only ones that \"exist\", and lower stage numbers will\n\tsimply loop around to them. When loading these stages, the function accesses\n\tthe data in \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e that lies before the statically\n\tallocated 5-element stages-of-scene array, which happens to encompass\n\tBorland C++'s locale and exception handling data, as well as a small bit of\n\tZUN's global variables. In particular, the obstacle/card HP on the tile I\n\thighlighted in \u003cspan style=\"color: green;\"\u003egreen\u003c/span\u003e corresponds to the\n\tlowest byte of the 32-bit RNG seed. If it weren't for that and the fact that\n\tthe obstacles/card HP on the few tiles before are similarly controlled by\n\tthe x86 segment values of certain initialization function addresses, these\n\tglitch stages would be completely deterministic across PC-98 systems, and\n\t\u003ci\u003etechnically\u003c/i\u003e canon… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tStage -4 is the only playable one here as it's the only stage to end up\n\tbelow the\n\t\u003ca href=\"/blog/2020-11-30\"\u003e📝 heap corruption limit of 102 stage objects\u003c/a\u003e.\n\tCompleting it loads Stage -3, which crashes with a \u003ccode\u003eDivide Error\u003c/code\u003e\n\tjust like it does if it's directly selected. Unsurprisingly, this happens\n\tbecause all 50 card bytes at that memory location are 0, so one division (or\n\tin this case, modulo operation) by the number of cards is enough to crash\n\tthe game.\u003cbr\u003e\n\tStage -5 is modulo'd to 0 and thus loads the first regular stage. The only\n\tapparent broken element there is the timer, which is handled by a completely\n\tdifferent function that still operates with a (0-based) stage ID value of\n\t-5. Completing the stage loads Stage -4, which also crashes, but only\n\tbecause its 61 cards naturally cause the\n\t\u003ca href=\"/blog/2020-11-30\"\u003e📝 stack overflow in the flip-in animation for any stage with more than 50 cards\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, the biggest and most bloated PC-98\n\tTouhou executable, fully decompiled! Next up: Finishing this game with the\n\tmain menu, and hoping I'll actually pull it off within 24 hours. (If I do,\n\twe might all have to thank \u003ca\n\thref=\"https://github.com/32th-System/ReC98/commit/db21033c827b0c932460e4fd0ccf0b224cbca206\"\u003e32th\n\tSystem\u003c/a\u003e, who independently decompiled half of the remaining 14\n\tfunctions…)\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-08-14T23:17:23Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-08-11",
      "url": "https://rec98.nmlgc.net/blog/2022-08-11",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-08\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-08-11\"\u003e\u003ctime datetime=\"2022-08-11T21:27:53Z\"\u003e2022-08-11 21:27\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0212\"\u003eP0212\u003c/a\u003e\n\t\t\tTH01 decompilation (Stage bonus and TOTLE screens, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d398a94...363fd54\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0213\"\u003eP0213\u003c/a\u003e\n\t\t\tTH01 decompilation (Stage bonus and TOTLE screens, part 2/2 + Data finalization, part 2/2 + FUUIN.EXE 100%)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/363fd54...158a91e\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eLeyDud, \u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e, GhostRiderCog, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Debug features that ZUN left in the shipped game.\"\u003edebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWow, it's been 3 days and I'm already back with an unexpectedly long post\n\tabout TH01's bonus point screens? 3 days used to take much longer in my\n\tprevious projects…\n\u003c/p\u003e\u003cp\u003e\n\tBefore I talk about graphics for the rest of this post, let's start with the\n\texact calculations for both bonuses. Touhou Wiki already got these right,\n\tbut it still makes sense to provide them here, in a format that allows you\n\tto cross-reference them with the source code more easily. For the\n\tcard-flipping stage bonus:\n\u003c/p\u003e\u003ctable class=\"numbers\"\u003e\u003ctr\u003e\n\t\u003cth\u003eTime\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003eStage timer\u003ccode\u003e * 3), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eContinuous\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003eHighest card combo\u003ccode\u003e * 100), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eBomb\u0026Player\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin(((\u003c/code\u003eLives\u003ccode\u003e * 200) + (\u003c/code\u003eBombs\u003ccode\u003e * 100)), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth\u003eSTAGE\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003e(Stage number\u003ccode\u003e - 1) * 200), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctfoot\u003e\u003ctr\u003e\n\t\u003cth\u003eBONUS Point\u003c/th\u003e\n\t\u003ctd\u003eSum of all above values * 10\u003c/td\u003e\n\u003c/tr\u003e\u003c/tfoot\u003e\u003c/table\u003e\u003cp\u003e\n\tThe boss stage bonus is calculated from the exact same metrics, despite half\n\tof them being labeled differently. The only actual differences are in the\n\thigher multipliers and in the cap for the stage number bonus. Why remove it\n\tif raising it high enough also effectively disables it?\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003ctable class=\"numbers\"\u003e\u003ctr\u003e\n\t\u003cth style=\"color:red;\"\u003eTime\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003eStage timer\u003ccode\u003e * 5), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth style=\"color:red;\"\u003eContinuous\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003eHighest card combo\u003ccode\u003e * 200), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth style=\"color:red;\"\u003eMIKOsan\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin(((\u003c/code\u003eLives\u003ccode\u003e * 500) + (\u003c/code\u003eBombs\u003ccode\u003e * 200)), 6553)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\t\u003cth style=\"color:red;\"\u003eClear\u003c/th\u003e\n\t\u003ctd\u003e\u003ccode\u003emin((\u003c/code\u003eStage number\u003ccode\u003e * 1000), 65530)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\u003ctfoot\u003e\u003ctr\u003e\n\t\u003cth style=\"color:red;\"\u003eTOTLE\u003c/th\u003e\n\t\u003ctd\u003eSum of all above values * 10\u003c/td\u003e\n\u003c/tr\u003e\u003c/tfoot\u003e\u003c/table\u003e\u003chr\u003e\u003cp\u003e\n\tThe transition between the gameplay and TOTLE screens is one of the more\n\timpressive effects showcased in this game, especially due to how wavy it\n\toften tends to look. Aside from the palette interpolation (which is, by the\n\tway, the first time ZUN wrote a correct interpolation algorithm between two\n\t4-bit palettes), the core of the effect is quite simple. With the TOTLE\n\timage blitted to VRAM page 1:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eShift the contents of a line on VRAM page 0 by 32 pixels, alternating\n\tthe shift direction between \u003ci\u003eright edge\u003c/i\u003e → \u003ci\u003eleft edge\u003c/i\u003e (even Y\n\tvalues) and the other way round (odd Y values)\u003c/li\u003e\n\t\u003cli\u003eKeep a cursor for the destination pixels on VRAM page 1 for every line,\n\tstarting at the respective opposite edge\u003c/li\u003e\n\t\u003cli\u003eBlit the 32 pixels at the VRAM page 1 cursor to the newly freed 32\n\tpixels on VRAM page 0, and advance the cursor towards the other edge\u003c/li\u003e\n\t\u003cli\u003eSuccessive line shifts will then include these newly blitted 32 pixels\n\tas well\u003c/li\u003e\n\t\u003cli\u003eRepeat \u003ccode\u003e(640 / 32) = 20\u003c/code\u003e times, after which all new pixels\n\twill be in their intended place\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo it's really more like two interlaced shift effects with opposite\n\tdirections, starting on different scanlines. No trigonometry involved at\n\tall.\n\u003c/p\u003e\u003cp\u003e\n\tHorizontally scrolling pixels on a single VRAM page remains one of the few\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 appropriate uses of the EGC in a fullscreen 640×400 PC-98 game\u003c/a\u003e,\n\tregardless of the copied block size. The few inter-page copies in this\n\teffect are also reasonable: With 8 new lines starting on each effect frame,\n\tup to \u003ccode\u003e(8 × \u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"amount of frames required to fully transfer a single line, see above\"\n\t\u003e20\u003c/span\u003e) =\u003c/code\u003e 160 lines are transferred at any given time, resulting\n\tin a maximum of \u003ccode\u003e(160 × \u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"alternating between page 0 and 1\"\n\t\u003e2\u003c/span\u003e × \u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"number of EGC operations required to cover 32 pixels\"\n\t\u003e2\u003c/span\u003e) =\u003c/code\u003e 640 VRAM page switches per frame for the newly\n\ttransferred pixels. Not that frame rate matters in this situation to begin\n\twith though, as the game is doing nothing else while playing this effect.\u003cbr\u003e\n\tWhat \u003ci\u003edoes\u003c/i\u003e sort of matter: Why 32 pixels every 2 frames, instead of 16\n\tpixels on every frame? There's no performance difference between doing one\n\thalf of the work in one frame, or two halves of the work in two frames. It's\n\tnot like the overhead of another loop has a serious impact here,\n\t\u003ci\u003eespecially\u003c/i\u003e with the PC-98 VRAM being said to have rather high\n\tlatencies. 32 pixels over 2 frames is also \u003ci\u003eharder\u003c/i\u003e to code, so ZUN\n\tmust have done it on purpose. Guess he really wanted to go for that 📽\n\t\u003ci\u003ecinematic 30 FPS look \u003c/i\u003e 📽 here… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-11-TH01-TOTLE-transition-original.webp?4f11c998\" preload=\"none\" controls data-title=\"Original, 32 pixels every 2 frames\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"140\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-transition-original.avi?f8dd262b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-11-TH01-TOTLE-transition-original.webm?da5e1435\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-11-TH01-TOTLE-transition-original.webm?03f44fb7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-11-TH01-TOTLE-transition-original.webm?40721872\" type=\"video/webm\"\u003eVideo of TH01's transition to the TOTLE screen, recorded starting from a black background and without palette interpolation, at the original 28.2 FPS frame rate. \u003ca href=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-transition-original.avi?f8dd262b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-11-TH01-TOTLE-transition-full-FPS.webp?4f11c998\" preload=\"none\" controls data-title=\"Full speed, 16 pixels every frame\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"140\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-transition-full-FPS.avi?6dd8a6ab\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-11-TH01-TOTLE-transition-full-FPS.webm?d589a447\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-11-TH01-TOTLE-transition-full-FPS.webm?24a355d1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-11-TH01-TOTLE-transition-full-FPS.webm?5cfdc2a4\" type=\"video/webm\"\u003eVideo of TH01's transition to the TOTLE screen, recorded starting from a black background and without palette interpolation, at the original 28.2 FPS frame rate. \u003ca href=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-transition-full-FPS.avi?6dd8a6ab\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tRemoving the palette interpolation and transitioning from a black screen\n\t\tto \u003ccode\u003eCLEAR3.GRP\u003c/code\u003e makes it a lot clearer how the effect works.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tOnce all the metrics have been calculated, ZUN animates each value with a\n\trather fancy left-to-right typing effect. As 16×16 images that use a single\n\t\u003cspan style=\"color:red\"\u003ebright-red color\u003c/span\u003e, these numbers would be\n\tperfect candidates for gaiji… except that ZUN wanted to render them at the\n\tmore natural Y positions of the labels inside \u003ccode\u003eCLEAR3.GRP\u003c/code\u003e that\n\tare far from aligned to the 8×16 text RAM grid. Not having been in the mood\n\tfor hardcoding another set of monochrome sprites as C arrays that day, ZUN\n\tmade the still reasonable choice of storing the image data for these numbers\n\tin the single-color .GRC form– yeah, no, of \u003ci\u003ecourse\u003c/i\u003e he once again\n\tchose the .PTN hammer, and its\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 16×16 \"quarter\" wrapper functions around nominal 32×32 sprites\u003c/a\u003e.\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cfigure class=\"side_by_side pixelated checkerboard\" style=\"width: 100%\"\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPABAPwAAAAAACH5BAUKAAEALAAAAAAgACAAQAJ7jAOpx43gXnIK0hWz3rtyU2FeM5LYNR2lmbJtyMAsaEW1PEu1jp6xT2v9hCogr8cJ7VyrYNEIlPlwnykRlVkot7nnS2QcZq3X4NKMTLvUw/OxbQu/5/Bxqln/6Pc6uRkHuELFExhVJjfYcYWnV1LGFAexpSSGNJnYV1IAADs=\"\n\t\t\talt=\".PTN sprite for the TOTLE metric digits of 0, 1, 2, and 3\"\n\t\t\tstyle=\"height: 128px;\"\u003e\u003cimg\n\t\t\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAIABAPwAAAAAACH5BAEKAAEALAAAAAAgACAAAAJ8jH+gmOrvWFsSWiANzDfmYHHYJ4IecpKNlrJd6Y4oFcvmTK+Vl5u96rsxHsAhcfdr1TohX/I3sTmlR9GTqlxem8Wu9wvmRplGW4rZOlfR2DZNyirDz0g5eIZ8AZWbKKnqZ2dlNZcFGIgD50SYxzhYRqjogvPR92dZCRVQAAA7\"\n\t\t\talt=\".PTN sprite for the TOTLE metric digits of 4, 5, 6, and 7\"\n\t\t\tstyle=\"height: 128px;\"\u003e\n\t\t\t\u003cimg\n\t\t\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAIABAPwAAAAAACH5BAEKAAEALAAAAAAgACAAAAJdjAOpcH2rHIovydXotJdVnnFY94GfATlmqlIQo5Ud+JpyGNdeS7MuO4tgbBuiBAUMnoJGpGgka/qgTk+suNvgbLrslnoMi8fksvmMTqvX7Lb7DY/L5/S6/Y7P6xsFADs=\"\n\t\t\talt=\".PTN sprite for the TOTLE metric digits of 8 and 9, filled with two blank quarters\"\n\t\t\tstyle=\"height: 128px;\"\u003e\n\t\u003c/figure\u003e\n\t\u003cfigcaption\u003eThe three 32×32 TOTLE metric digit sprites inside\n\t\u003ccode\u003eNUMB.PTN\u003c/code\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhy do I bring up such a detail? What's actually going on there is that ZUN\n\tloops through and blits each digit from 0 to 9, and then continues the loop\n\twith \"digit\" numbers from 10 to 19, stopping before the number whose ones\n\tdigit equals the one that should stay on screen. No problem with that in\n\ttheory, and the .PTN \u003ci\u003esprite\u003c/i\u003e selection is correct… but the .PTN\n\t\u003ci\u003equarter\u003c/i\u003e selection isn't, as ZUN wrote \u003ccode\u003e(digit % 4)\u003c/code\u003e\n\tinstead of the correct \u003ccode\u003e((digit % 10) % 4)\u003c/code\u003e.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Since .PTN quarters are indexed in a row-major\n\tway, the 10-19 part of the loop thus ends up blitting\n\t\u003cstrong style=\"color:red\"\u003e2\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e3\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e0\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e1\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e6\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e7\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e4\u003c/strong\u003e →\n\t\u003cstrong style=\"color:red\"\u003e5\u003c/strong\u003e →\n\t\u003ci\u003e(nothing)\u003c/i\u003e:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-11-TH01-TOTLE-metrics.webp?8f24c26a\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"344\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-metrics.avi?8bf331e8\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-11-TH01-TOTLE-metrics.webm?1729f51c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-11-TH01-TOTLE-metrics.webm?8493d028\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-11-TH01-TOTLE-metrics.webm?78447df4\" type=\"video/webm\"\u003eVideo of the per-metric digit animation in TH01's TOTLE screen, slowed down to one digit per frame and demonstrating the counting quirk on the second digit loop. \u003ca href=\"/blog/static/video/zmbv/2022-08-11-TH01-TOTLE-metrics.avi?8bf331e8\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"49\" data-title=\"Empty quarter\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThis footage was slowed down to show one sprite blitting operation per\n\t\tframe. The actual game waits a hardcoded 4 milliseconds between each\n\t\tsprite, so even theoretically, you would only see roughly every\n\t\t4\u003csup\u003eth\u003c/sup\u003e digit. And yes, we can also observe the empty quarter\n\t\there, only blitted if one of the digits is a 9.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSeriously though? If the deadline is looming and you've got to rush\n\t\u003ci\u003esome\u003c/i\u003e part of your game, a standalone screen that doesn't affect\n\tanything is \u003ci\u003ethe\u003c/i\u003e best place to pick. At 4 milliseconds per digit, the\n\tanimation goes by so fast that this quirk might even \u003ci\u003eadd\u003c/i\u003e to its\n\tperceived fanciness. It's exactly the reason why I've always been rather\n\tcareful with labeling such quirks as \"bugs\". And in the end, the code does\n\tperform one more blitting call after the loop to make sure that the correct\n\tdigit remains on screen.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe remaining ¾ of the second push went towards transferring the final data\n\tdefinitions from ASM to C land. Most of the details there paint a rather\n\tdepressing picture about ZUN's original code layout and the bloat that came\n\twith it, but it did end on a real highlight. There was some unused data\n\tbetween ZUN's non-master.lib VSync and text RAM code that I just moved away\n\tin September 2015 without taking a closer look at it. Those bytes kind of\n\tlook like another hardcoded 1bpp image though… wait, \u003ci\u003ewhat\u003c/i\u003e?!\n\u003c/p\u003e\u003cfigure class=\"pixelated checkerboard\"\u003e\n\t\u003cimg src=\"data:image/gif;base64,R0lGODlhIAAQAPABAP///wAAACH5BAUKAAEALAAAAAAgABAAAAI7BIKpy4123IoyQVpNtXcH6F1aBpKiOTpi16zSyk5wC5fKTLtqnPOMlsp9bD/OK3LaBFGeYZNIajqlkgIAOw==\"\n\talt=\"An unused mouse cursor sprite found in all of TH01's binaries\"\n\tstyle=\"height: 128px;\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tLovely! With no mouse-related code left in the game otherwise, this cursor\n\tsprite provides some great fuel for wild fan theories about TH01's\n\tdevelopment history:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eCould ZUN have \u003ca href=\"/blog/2019-11-06\"\u003e📝 stolen\u003c/a\u003e the basic PC-98\n\tVSync or text RAM function code from a source that also implemented mouse\n\tsupport?\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://twitter.com/gensakudan/status/1557594118217515008\"\u003eDid\n\the have a mouse-controlled level editor during development?\u003c/a\u003e It's highly\n\tlikely that he had \u003ci\u003esomething\u003c/i\u003e, given all the\n\t\u003ca href=\"/blog/2020-11-30\"\u003e📝 bit twiddling seen in the \u003ccode\u003eSTAGE?.DAT\u003c/code\u003e format\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eOr was this game actually meant to have mouse-controllable portions at\n\tsome point during development? Even if it would have just been the\n\tmenus.\u003c/li\u003e\n\u003c/ol\u003e\u003chr\u003e\u003cp\u003e\n\t… Actually, you know what, with all shared data moved to C land, I might as\n\twell finish \u003ccode\u003eFUUIN.EXE\u003c/code\u003e right now. The last secret hidden in its\n\t\u003ccode\u003emain()\u003c/code\u003e function: Just like \u003ccode\u003eGAME.BAT\u003c/code\u003e supports\n\tlaunching the game in various debug modes from the DOS command line,\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e can directly launch one of the game's endings. As\n\tlong as the MDRV2 driver is installed, you can enter\n\t\u003ccode\u003efuuin\u0026nbsp;t1\u003c/code\u003e for the 魔界/Makai Good Ending, or\n\t\u003ccode\u003efuuin\u0026nbsp;t\u003c/code\u003e for 地獄/Jigoku Good Ending.\u003cbr\u003e\n\tUnfortunately, the command-line parameter can only control the route.\n\tChoosing between a Good or Bad Ending is still done exclusively through\n\tTH01's resident structure, and the \u003ccode\u003econtinues_per_scene\u003c/code\u003e array in\n\tparticular. But if you pre-allocate that structure somehow and set one of\n\tthe members to a nonzero value, it would work. \u003ca\n\thref=\"https://www.youtube.com/watch?v=3hMHZJFEHIY\"\u003eTrainers, anyone?\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tAlright, gotta get back to the code if I want to have any chance of\n\tfinishing this game before the 15\u003csup\u003eth\u003c/sup\u003e… Next up: The final 17\n\tfunctions in \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e that tie everything together and add\n\tsome more debug features on top.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-08-08\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-08-11T21:27:53Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-08-08",
      "url": "https://rec98.nmlgc.net/blog/2022-08-08",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-07-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-08-08\"\u003e\u003ctime datetime=\"2022-08-08T18:23:19Z\"\u003e2022-08-08 18:23\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0207\"\u003eP0207\u003c/a\u003e\n\t\t\tTH01 decompilation (YuugenMagan, part 1/5: Preparation)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/454c105...c26ef4b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0208\"\u003eP0208\u003c/a\u003e\n\t\t\tTH01 decompilation (YuugenMagan, part 2/5: Helper functions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c26ef4b...239a3ec\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0209\"\u003eP0209\u003c/a\u003e\n\t\t\tTH01 decompilation (YuugenMagan, part 3/5: Main function)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/239a3ec...5030867\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0210\"\u003eP0210\u003c/a\u003e\n\t\t\tTH01 decompilation (YuugenMagan, part 4/5: Eye opening/closing + 邪 colors)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5030867...149fbca\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0211\"\u003eP0211\u003c/a\u003e\n\t\t\tTH01 decompilation (YuugenMagan, part 5/5: Quirk research + Data finalization, part 1/2 + Common part of endings)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/149fbca...d398a94\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eGhostPhanom, Yanga, \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e, \u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuugenmagan\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 魔界/Makai route.\"\u003eyuugenmagan\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\u003cp\u003e\n\tWhew, TH01's boss code just had to end with another beast of a boss, taking\n\tway longer than it should have and leaving uncomfortably little time for the\n\trest of the game. Let's get right into the overview of YuugenMagan, the most\n\tsequential and scripted battle in this game:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe fight consists of 14 phases, numbered (of course) from 0 to 13.\n\tUnlike all other bosses, the \"entrance phase\" 0 is a proper gameplay-enabled\n\tpart of the fight itself, which is why I also count it here.\u003c/li\u003e\n\t\u003cli\u003eYuugenMagan starts with 16 HP, second only to Sariel's 18+6. The HP bar\n\tvisualizes the HP threshold for the end of phases 3 (white part) and 7\n\t(red-white part), respectively.\u003c/li\u003e\n\t\u003cli\u003eAll even-numbered phases change the color of the 邪 kanji in the stage\n\tbackground, and don't check for collisions between the Orb and any eye.\n\tAlmost all of them consequently don't feature an attack, except for phase\n\t0's 1-pixel lasers, spawning symmetrically from the left and right edges of\n\tthe playfield towards the center. Which means that yes, YuugenMagan is in\n\tfact invincible during this first attack.\u003c/li\u003e\n\t\u003cli\u003eAll other attacks are part of the odd-numbered phases:\u003cul\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 1:\u003c/strong\u003e Slow pellets from the lateral eyes. Ends\n\t\tat 15 HP.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 3:\u003c/strong\u003e Missiles from the southern eyes, whose\n\t\tangles first shift away from Reimu's tracked position and then towards\n\t\tit. Ends at 12 HP.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 5:\u003c/strong\u003e Circular pellets sprayed from the lateral\n\t\teyes. Ends at 10 HP.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 7:\u003c/strong\u003e Another missile pattern, but this time\n\t\twith both eyes shifting their missile angles by the same\n\t\t(counter-)clockwise delta angles. Ends at 8 HP.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 9:\u003c/strong\u003e The 3-pixel 3-laser sequence from the\n\t\tnorthern eye. Ends at 2 HP.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 11:\u003c/strong\u003e Spawns the pentagram with one corner out\n\t\tof every eye, then gradually shrinks and moves it towards the center of\n\t\tthe playfield. Not really an \"attack\" (surprise) as the pentagram can't\n\t\treach the player during this phase, but collision detection is\n\t\ttechnically already active here. Ends at 0 HP, marking the earliest\n\t\tpoint where the fight itself can possibly end.\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 13:\u003c/strong\u003e Runs through the parallel \"pentagram\n\t\tattack phases\". The first five consist of the pentagram alternating its\n\t\tspinning direction between clockwise and counterclockwise while firing\n\t\tpellets from each of the five star corners. After that, the pentagram\n\t\tslams itself into the player, before YuugenMagan \u003ci\u003eloops back to phase\n\t\t10\u003c/i\u003e to spawn a new pentagram. On the next run through phase 13, the\n\t\tpentagram grows larger and immediately slams itself into the player,\n\t\tbefore starting a new pentagram attack phase cycle with another loop\n\t\tback to phase 10.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eSince the HP bar fills up in a phase with no collision detection,\n\tYuugenMagan is immune to\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 test/debug mode heap corruption\u003c/a\u003e. It's\n\tgenerally impossible to get YuugenMagan's HP into negative numbers, with\n\tcollision detection being disabled every other phase, and all odd-numbered\n\tphases ending immediately upon reaching their HP threshold.\u003c/li\u003e\n\t\u003cli\u003eAll phases until the very last one have a timeout condition, independent\n\tfrom YuugenMagan's current HP:\u003cul\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 0:\u003c/strong\u003e 331 frames\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 1:\u003c/strong\u003e 1101 frames\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhases 2, 4, 6, 8, 10, and 12:\u003c/strong\u003e 70 frames each\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhases 3 and 7:\u003c/strong\u003e 5 iterations of the pattern, or\n\t\t1845 frames each\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 5:\u003c/strong\u003e 5 iterations of the pattern, or 2230\n\t\tframes\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 9:\u003c/strong\u003e The full duration of the sequence, or 491\n\t\tframes\u003c/li\u003e\n\t\t\u003cli\u003e\u003cstrong\u003ePhase 11:\u003c/strong\u003e Until the pentagram reached its target\n\t\tposition, or 221 frames\u003c/li\u003e\n\t\u003c/ul\u003e\n\tThis makes it possible to reach phase 13 without dealing a single point of\n\tdamage to YuugenMagan, after almost exactly 2½ minutes on any difficulty.\n\tYour actual time will certainly be higher though, as you \u003ci\u003ewill\u003c/i\u003e have to\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e at least once during the attempt.\n\tAnd let's be real, you're \u003ci\u003every\u003c/i\u003e likely to subsequently lose a\n\tlife.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAt a pixel-perfect 81×61 pixels, the Orb hitboxes are laid out rather\n\tgenerously this time, reaching quite a bit outside the 64×48 eye sprites:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cimg src=\"/blog/static/2022-08-08-TH01-YuugenMagan-eye-hitboxes.png?b442203d\" alt=\"TH01 YuugenMagan's hitboxes.\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd that's about the only positive thing I can say about a position\n\tcalculation in this fight. Phase 0 already starts with the lasers being off\n\tby 1 pixel from the center of the iris. Sure, 28 may be a nicer number to\n\tadd than 29, but the result won't be byte-aligned either way? This is\n\tfollowed by the eastern laser's hitbox somehow being 24 pixels larger than\n\tthe others, stretching a rather unexpected 70 pixels compared to the 46 of\n\tevery other laser.\u003cbr\u003e\n\tOn a more hilarious note, the eye closing keyframe contains the following\n\t(pseudo-)code, comprising the only real accidentally \"unused\" danmaku\n\tsubpattern in TH01:\n\u003c/p\u003e\u003cpre\u003e// Did you mean \"\u003e= RANK_HARD\"?\nif(rank == RANK_HARD) {\n\teye_north.fire_aimed_wide_5_spread();\n\teye_southeast.fire_aimed_wide_5_spread();\n\teye_southwest.fire_aimed_wide_5_spread();\n\n\t// Because this condition can never be true otherwise.\n\t// As a result, no pellets will be spawned on Lunatic mode.\n\t// (There is another Lunatic-exclusive subpattern later, though.)\n\tif(rank == RANK_LUNATIC) {\n\t\teye_west.fire_aimed_wide_5_spread();\n\t\teye_east.fire_aimed_wide_5_spread();\n\t}\n}\u003c/pre\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-0-Hard.webp?b9673029\" preload=\"none\" controls data-title=\"Hard, original\" loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"305\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Hard.avi?ef0019a5\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-0-Hard.webm?0b5d7cd5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-0-Hard.webm?a7df4874\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-0-Hard.webm?9ae4bae8\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 0 on Hard mode, with hitbox overlays. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Hard.avi?ef0019a5\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.webp?9881325b\" preload=\"none\" controls data-title=\"Lunatic, original\" loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"305\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.avi?02541288\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.webm?1718678d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.webm?2a7f81e3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.webm?accd501b\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 0 on Lunatic mode, with hitbox overlays. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic.avi?02541288\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.webp?9881325b\" preload=\"none\" controls data-title=\"Lunatic, dequirked subpattern condition\" loop data-active width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"305\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.avi?99649f62\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.webm?ad772f50\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.webm?0c3025cc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.webm?f6998cd8\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 0 on Lunatic mode, with hitbox overlays and the full intended pellet pattern. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-0-Lunatic-unused.avi?99649f62\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tFeaturing the weirdly extended hitbox for the eastern laser, as well as\n\t\tan initial Reimu position that points out the disparity between\n\t\tbyte-aligned rendering and the internal coordinates one final time.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tAfter a few utility functions that look more like a quickly abandoned\n\trefactoring attempt, we quickly get to the main attraction: YuugenMagan\n\tcombines the entire boss script and most of the pattern code into a single\n\t2,634-instruction function, totaling 9,677 bytes inside\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e. For comparison, ReC98's version of this code\n\tconsists of at least 49 functions, excluding those I had to add to work\n\taround ZUN's little inconsistencies, or the ones I added for stylistic\n\treasons.\u003cbr\u003e\n\tIn fact, this function is so large that Turbo C++ 4.0J refuses to generate\n\tassembly output for it via the \u003ccode\u003e-S\u003c/code\u003e command-line option, aborting\n\twith a \u003ccode\u003eCompiler table limit exceeded in function\u003c/code\u003e error.\n\tContrary to what the \u003ci\u003eBorland C++ 4.0 User Guide\u003c/i\u003e suggests, this\n\tinstance of the error is not at all related to the number of function bodies\n\tor any metric of algorithmic complexity, but is simply a result of the\n\tcompiler's internal text representation for a single function overflowing a\n\t64 KiB memory segment. Merely shortening the names of enough identifiers\n\twithin the function can help to get that representation down below 64 KiB.\n\tIf you encounter this error during regular software development, you might\n\tinterpret it as the compiler's roundabout way of telling you that it inlined\n\tway more function calls than you probably wanted to have inlined. Because\n\t\u003ci\u003eyou\u003c/i\u003e definitely \u003ci\u003ewon't\u003c/i\u003e explicitly spell out such a long function\n\tin newly-written code, right?\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAt least it wasn't the worst copy-pasting job in this\n\tgame; that trophy still goes to \u003ca href=\"/blog/2022-05-31\"\u003e📝 Elis\u003c/a\u003e. And\n\twhile the tracking code for adjusting an eye's sprite according to the\n\tplayer's relative position is one of the main causes behind all the bloat,\n\tit's also 100% consistent, and might have been an inlined class method in\n\tZUN's original code as well.\n\u003c/p\u003e\u003cp\u003e\n\tThe clear highlight in this fight though? \u003ci\u003eAlmost no coordinate is\n\tprecisely calculated where you'd expect it to be.\u003c/i\u003e In particular, all\n\tbullet spawn positions completely ignore the direction the eyes are facing\n\tto:\n\u003c/p\u003e\u003cfigure class=\"th01_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-08-08-TH01-YuugenMagan-inaccurate-bottom-center.png?49a2a6a0\" alt=\"Pellets unexpectedly spawned at the exact\n\tbottom center of an eye\"\u003e\n\t\u003cfigcaption\u003eCombining the bottom of the pupil with the exact horizontal\n\tcenter of the sprite as a whole might sound like a good idea, but looks\n\tespecially wrong if the eye is facing right.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cfigure class=\"th01_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-08-08-TH01-YuugenMagan-inaccurate-missiles.png?4d7a6d3f\" alt=\"Missile spawn positions in the TH01\n\tYuugenMagan fight\"\u003e\n\t\u003cfigcaption\u003eHere it's the other way round: OK for a right-facing eye, really\n\twrong for a left-facing one.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cfigure class=\"th01_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-08-08-TH01-YuugenMagan-inaccurate-3-pixel-laser.png?033dcaa5\" alt=\"Spawn position of the 3-pixel laser in the\n\tTH01 YuugenMagan fight\"\u003e\n\t\u003cfigcaption\u003eDude, the eye is even \u003ci\u003esupposed\u003c/i\u003e to track the laser in this\n\tone!\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cfigure class=\"th01_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-08-08-TH01-YuugenMagan-inaccurate-pentagram.png?62c0fdf9\" alt=\"The final center position of the regular\n\tpentagram in the TH01 YuugenMagan fight\"\u003e\n\t\u003cfigcaption\u003eHint: That's not the center of the playfield. At least the\n\tpellets spawned from the corners are sort of correct, but with the corner\n\tcalculates precomputed, you could only get them wrong on\n\tpurpose.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tDue to their effect on gameplay, these inaccuracies can't even be called\n\t\"bugs\", and made me devise a new \"quirk\" category instead. More on that in\n\tthe TH01 100% blog post, though.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhile we did see an \u003ci\u003eaccidentally\u003c/i\u003e unused bullet pattern earlier, I can\n\tnow say with certainty that there are no \u003ci\u003etruly\u003c/i\u003e unused danmaku\n\tpatterns in TH01, i.e., pattern code that exists but is never called.\n\tHowever, the code for YuugenMagan's phase 5 reveals another small piece of\n\tdanmaku design \u003ci\u003eintention\u003c/i\u003e that never shows up within the parameters of\n\tthe original game.\u003cbr\u003e\n\tBy default, pellets are clipped when they fly past the top of the playfield,\n\twhich we can clearly observe for the first few pellets of this pattern.\n\tInterestingly though, the second subpattern actually configures its pellets\n\tto fall straight down from the top of the playfield instead. You never see\n\tthis happening in-game because ZUN limited that subpattern to a downwards\n\tangle range of \u003ccode\u003e0x73\u003c/code\u003e or 162°, resulting in none of its pellets\n\tever getting close to the top of the playfield. If we extend that range to a\n\tfull 360° though, we can see how ZUN might have originally planned the\n\tpattern to end:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.webp?972f7b7f\" preload=\"none\" controls data-title=\"Easy\" loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"665\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.avi?d0105977\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.webm?997b846f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.webm?d6fe15eb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.webm?cab25769\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 5 patterns on Easy Mode, with the second variation running for a full 360° rather than the original 162° to reveal an unused piece of danmaku design. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Easy.avi?d0105977\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"231\" data-title=\"Second subpattern\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"347\" data-title=\"Original end\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.webp?b9e9c3af\" preload=\"none\" controls data-title=\"Normal\" loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"665\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.avi?0cebbf68\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.webm?8d59837d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.webm?764a9d3b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.webm?4504af9c\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 5 patterns on Normal Mode, with the second variation running for a full 360° rather than the original 162° to reveal an unused piece of danmaku design. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Normal.avi?0cebbf68\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"231\" data-title=\"Second subpattern\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"347\" data-title=\"Original end\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.webp?29df60e7\" preload=\"none\" controls data-title=\"Hard\" loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"665\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.avi?269aee13\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.webm?cff729d2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.webm?f36d9360\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.webm?95fe8d63\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 5 patterns on Hard Mode, with the second variation running for a full 360° rather than the original 162° to reveal an unused piece of danmaku design. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Hard.avi?269aee13\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"231\" data-title=\"Second subpattern\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"347\" data-title=\"Original end\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.webp?a1b72c74\" preload=\"none\" controls data-title=\"Lunatic\" loop data-active width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"665\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.avi?5eecf0b1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.webm?cdb073ec\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.webm?c74e6983\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.webm?7287d454\" type=\"video/webm\"\u003eVideo of TH01 YuugenMagan's phase 5 patterns on Lunatic Mode, with the second variation running for a full 360° rather than the original 162° to reveal an unused piece of danmaku design. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-phase-5-reconstructed-Lunatic.avi?5eecf0b1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"231\" data-title=\"Second subpattern\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"347\" data-title=\"Original end\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eYuugenMagan's phase 5 patterns on every difficulty, with the\n\tsecond subpattern extended to reveal the different pellet behavior that\n\tremained in the final game code. In the original game, the eyes would stop\n\tspawning bullets on the marked frame.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tIf we also disregard everything else about YuugenMagan that fits the\n\tupcoming definition of \u003ci\u003equirk\u003c/i\u003e, we're left with 6 \"fixable\" bugs, all\n\tof which are a symptom of general blitting and unblitting laziness. Funnily\n\tenough, they can all be demonstrated within a short 9-second part of the\n\tfight, from the end of phase 9 up until the pentagram starts spinning in\n\tphase 13:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-blitting-bugs.webp?9e3ba5c5\" preload=\"none\" controls loop width=\"640\" height=\"352\" data-fps=\"56.423132\" data-frame-count=\"550\" style=\"aspect-ratio: 640 / 352\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-blitting-bugs.avi?b2ec861e\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-blitting-bugs.webm?307b5e7e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-blitting-bugs.webm?ec41b6ce\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-blitting-bugs.webm?a103aa63\" type=\"video/webm\"\u003eVideo demonstrating how ZUN's blitting-related laziness manifests itself in a total of 4 bugs in the TH01 YuugenMagan fight. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-blitting-bugs.avi?b2ec861e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"5\" data-title=\"1\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"9\" data-title=\"2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"38\" data-title=\"3\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"248\" data-title=\"4\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"332\" data-title=\"5\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"400\" data-title=\"6\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003col\u003e\n\t\u003cli\u003eGeneral flickering whenever any sprite overlaps an eye. This is caused\n\tby only reblitting each eye every 3 frames, and is an issue all throughout\n\tthe fight. You might have already spotted it in the videos above.\u003c/li\u003e\n\t\u003cli\u003eEach of the two lasers is unblitted and blitted individually instead of\n\teach operation being done for both lasers together. Remember how\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 ZUN unblits 32 horizontal pixels for every row of a line regardless of its width\u003c/a\u003e?\n\tThat's why the top part of the left, right-moving laser is never visible,\n\tbecause it's blitted before the other laser is unblitted.\u003c/li\u003e\n\t\u003cli\u003eZUN forgot to unblit the lasers when phase 9 ends. This footage was\n\trecorded by pressing ↵\u0026nbsp;Return in test mode (\u003ccode\u003egame t\u003c/code\u003e or\n\t\u003ccode\u003egame d\u003c/code\u003e), and it's probably impossible to achieve this during\n\tactual gameplay without TAS techniques. You would have to deal the required\n\t6 points of damage within 491 frames, with the eye being invincible during\n\t240 of them. Simply shooting up an Orb with a horizontal velocity of 0 would\n\talso only work a single time, as boss entities always repel the Orb with a\n\thorizontal velocity of ±4.\u003c/li\u003e\n\t\u003cli\u003eThe shrinking pentagram is unblitted after the eyes were blitted,\n\tadding another guaranteed frame of flicker on top of the ones in 1). Like in\n\t2), the blockiness of the holes is another result of unblitting 32 pixels\n\tper row at a time.\u003c/li\u003e\n\t\u003cli\u003eAnother missing unblitting call in a phase transition, as the pentagram\n\tswitches from its not quite correctly interpolated shrunk form to a regular\n\tstar polygon with a radius of 64 pixels. Indirectly caused by the massively\n\tbloated coordinate calculation for the shrink animation being done\n\tseparately for the unblitting and blitting calls. Instead of, y'know, just\n\tdoing it once and storing the result in variables that can later be\n\treused.\u003c/li\u003e\n\t\u003cli\u003eThe pentagram is not reblitted at all during the first 100 frames of\n\tphase 13. During that rather long time, it's easily possible to remove\n\tit from VRAM completely by covering its area with player shots. Or \u003cspan\n\tstyle=\"color: red\"\u003eHARRY UP\u003c/span\u003e pellets.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tDefinitely an appropriate end for this game's entity blitting code.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e I'm \u003ci\u003ereally\u003c/i\u003e looking forward to writing a\n\tproper sprite system for the Anniversary Edition…\n\u003c/p\u003e\u003cp\u003e\n\tAnd just in case you were wondering about the hitboxes of these pentagrams\n\tas they slam themselves into Reimu:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.webp?ef6bf331\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"42.31725\" data-frame-count=\"894\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.avi?595ddad8\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.webm?bfaf5747\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.webm?003a56e5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.webm?c241044d\" type=\"video/webm\"\u003eVideo of the pentagram hitboxes in the TH01 YuugenMagan fight. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-pentagram-hitboxes.avi?595ddad8\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\t62 pixels on the X axis, centered around each corner point of the star, 16\n\tpixels below, and extending infinitely far up. The latter part becomes\n\tespecially devious because the game \u003ci\u003ealways\u003c/i\u003e collision-detects\n\t\u003ci\u003eall\u003c/i\u003e 5 corners, regardless of whether they've already clipped through\n\tthe bottom of the playfield. The simultaneously occurring shape distortions\n\tare simply a result of the line drawing function's rather poor\n\tre-interpolation of any line that runs past the 640×400 VRAM boundaries;\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 I described that in detail back when I debugged the shootout laser crash\u003c/a\u003e.\n\tIronically, using fixed-size hitboxes for a variable-sized pentagram means\n\tthat the larger one is easier to dodge.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe final puzzle in TH01's boss code comes\n\t\u003ca href=\"/blog/2022-07-17\"\u003e📝 once again\u003c/a\u003e in the form of weird hardware\n\tpalette changes. The \u003cspan class=\"ja\"\u003e邪\u003c/span\u003e kanji on the background\n\timage goes through various colors throughout the fight, which ZUN\n\timplemented by gradually incrementing and decrementing either a single one\n\tor none of the color's three 4-bit components at the beginning of each\n\teven-numbered phase. The resulting color sequence, however, doesn't\n\t\u003ci\u003equite\u003c/i\u003e seem to follow these simple rules:\n\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003ePhase 0\u003c/strong\u003e: \u003ccode\u003e#DD5\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #DD5;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003ePhase 2\u003c/strong\u003e: \u003ccode\u003e#0DF\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #0DF;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003ePhase 4\u003c/strong\u003e: \u003ccode\u003e#F0F\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #F0F;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003ePhase 6\u003c/strong\u003e: \u003ccode\u003e#00F\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #00F;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e, but at the\n\t\u003ci\u003eend\u003c/i\u003e of the phase?!\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003ePhase 8\u003c/strong\u003e: \u003ccode\u003e#0FF\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #0FF;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e, at the \u003ci\u003estart\u003c/i\u003e\n\tof the phase, \u003ccode\u003e#0F5\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #0F5;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e, at the \u003ci\u003eend\u003c/i\u003e!?\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003ePhase 10\u003c/strong\u003e: \u003ccode\u003e#FF5\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #FF5;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e, at the start of\n\tthe phase, \u003ccode\u003e#F05\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #F05;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e, at the end\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003eSecond repetition of phase 12\u003c/strong\u003e: \u003ccode\u003e#005\u003c/code\u003e\n\t\t\u003cstrong lang=\"ja\" style=\"\n\t\t\tbackground: linear-gradient(135deg, #600 0%, #018 100%);\n\t\t\tpadding: 0.25em;\n\t\t\tborder-radius: 0.25em;\n\t\t\tcolor: #005;\n\t\t\tline-height: 2em;\n\t\t\"\u003e邪\u003c/strong\u003e\n\tshortly after the start of the phase?! \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAdding some debug output sheds light on what's going on there:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-08-08-TH01-YuugenMagan-邪-color.webp?8ce21778\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"911\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-邪-color.avi?e3086f8b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-08-08-TH01-YuugenMagan-邪-color.webm?f981f5cc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-08-08-TH01-YuugenMagan-邪-color.webm?243294bd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-08-08-TH01-YuugenMagan-邪-color.webm?5e81361d\" type=\"video/webm\"\u003eVideo of the 邪 color shifts in the TH01 YuugenMagan fight, together with debug output to demonstrate how the final colors are a result of stage palette overflows. \u003ca href=\"/blog/static/video/zmbv/2022-08-08-TH01-YuugenMagan-邪-color.avi?e3086f8b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"0\" data-title=\"0\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"1\" data-title=\"2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"71\" data-title=\"4\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"141\" data-title=\"6\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"211\" data-title=\"8\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"281\" data-title=\"10\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"421\" data-title=\"12, #2\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tSince each iteration of phase 12 adds 63 to the red component, integer\n\t\toverflow will cause the color to infinitely alternate between dark-blue\n\t\tand red colors on every 2.03 iterations of the pentagram phase loop. The\n\t\t65th iteration will therefore be the first one with a dark-blue color\n\t\tfor a third iteration in a row – just in case you manage to stall the\n\t\tfight for that long.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tYup, ZUN had so much trust in the color clamping done by his hardware\n\tpalette functions that he did not clamp the increment operation on the\n\t\u003ccode\u003estage_palette\u003c/code\u003e itself. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Therefore, the 邪\n\tcolors and even the timing of their changes from Phase 6 onwards are\n\t\"defined\" by wildly incrementing color components beyond their intended\n\tdomain, so much that even the underlying signed 8-bit integer ends up\n\toverflowing. Given that the decrement operation on the\n\t\u003ccode\u003estage_palette\u003c/code\u003e \u003ci\u003eis\u003c/i\u003e clamped though, this might be another\n\tone of those accidents that ZUN deliberately left in the game,\n\t\u003ca href=\"/blog/2022-07-10\"\u003e📝 similar to the conclusion I reached with infinite bumper loops\u003c/a\u003e.\u003cbr\u003e\n\tBut guess what, that's also the last time we're going to encounter this type\n\tof palette component domain quirk! Later games use master.lib's 8-bit\n\tpalette system, which keeps the comfort of using a single byte per\n\tcomponent, but shifts the actual hardware color into the top 4 bits, leaving\n\tthe bottom 4 bits for added precision during fades.\n\u003c/p\u003e\u003cp\u003e\n\tOK, but \u003ci\u003enow\u003c/i\u003e we're done with TH01's bosses! 🎉That was the\n\t8\u003csup\u003eth\u003c/sup\u003e PC-98 Touhou boss in total, leaving 23 to go.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith all the necessary research into these quirks going well into a fifth\n\tpush, I spent the remaining time in that one with transferring most of the\n\tdata between YuugenMagan and the upcoming rest of \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e\n\tinto C land. This included the one piece of technical debt in TH01 we've\n\tbeen carrying around since March 2015, as well as the final piece of the\n\tending sequence in \u003ccode\u003eFUUIN.EXE\u003c/code\u003e. Decompiling that executable's\n\t\u003ccode\u003emain()\u003c/code\u003e function in a meaningful way requires pretty much all\n\tremaining data from \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e to also be moved into C land,\n\tjust in case you were wondering why we're stuck at 99.46% there.\u003cbr\u003e\n\tOn a more disappointing note, the static initialization code for the\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 5 boss entity slots\u003c/a\u003e ultimately revealed why\n\tYuugenMagan's code is as bloated and redundant as it is: The 5 slots really\n\tare 5 distinct variables rather than a single 5-element array. That's why\n\tZUN explicitly spells out all 5 eyes every time, because the array he could\n\thave just looped over simply didn't exist. 😕 And while these slot variables\n\t\u003ci\u003eare\u003c/i\u003e stored in a contiguous area of memory that I could just have\n\ttaken the address of and then indexed it as \u003ci\u003eif\u003c/i\u003e it were an array, I\n\tdidn't want to annoy future port authors with what would technically be\n\tout-of-bounds array accesses for purely stylistic reasons. At least it\n\twasn't that big of a deal to rewrite all boss code to use these distinct\n\tvariables, although I certainly had to get a bit creative with Elis.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Finding out how many points we got in totle, and hoping that ZUN\n\tdidn't hide more unexpected complexities in the remaining 45 functions of\n\tthis game. If you have \u003cscript\u003eformatCurrency(1000)\u003c/script\u003e\u003cnoscript\u003e10.00\u0026nbsp;€\u003c/noscript\u003e to spare, there are two ways\n\tin which that amount of money would help right now:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eI'm expecting another \u003cscript\u003eformatCurrency(3000)\u003c/script\u003e\u003cnoscript\u003e30.00\u0026nbsp;€\u003c/noscript\u003e subscription transaction\n\tfrom Yanga before the 15th, which would leave \u003cscript\u003eformatCurrency(1000)\u003c/script\u003e\u003cnoscript\u003e10.00\u0026nbsp;€\u003c/noscript\u003e to\n\tround out one final TH01 RE push. With that, there'd be a total of 5 left in\n\tthe backlog, which should be enough to get the rest of this game done.\u003c/li\u003e\n\t\u003cli\u003eI \u003ci\u003ereally\u003c/i\u003e need to address the performance and usability issues\n\twith all the small videos in this blog. Just look at the video immediately\n\tabove, where I disabled the controls because they would cover the debug text\n\tat the bottom… \u003cstrong\u003eEdit (2022-10-31):\u003c/strong\u003e… which no longer is an\n\tissue with our \u003ca href=\"/blog/2022-10-31\"\u003e📝 custom video player\u003c/a\u003e.\u003cbr\u003e\n\tI already reserved this month's anonymous \u003cscript\u003eformatCurrency(5000)\u003c/script\u003e\u003cnoscript\u003e50.00\u0026nbsp;€\u003c/noscript\u003e contribution for this work, so it would take another \u003cscript\u003eformatCurrency(1000)\u003c/script\u003e\u003cnoscript\u003e10.00\u0026nbsp;€\u003c/noscript\u003e to be turned into a full push.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-07-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-08-08T18:23:19Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-07-17",
      "url": "https://rec98.nmlgc.net/blog/2022-07-17",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-08\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-07-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-07-17\"\u003e\u003ctime datetime=\"2022-07-17T19:20:47Z\"\u003e2022-07-17 19:20\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0205\"\u003eP0205\u003c/a\u003e\n\t\t\tTH01 decompilation (Mima, part 1/2: Patterns 1-4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3259190...327730f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0206\"\u003eP0206\u003c/a\u003e\n\t\t\tTH01 decompilation (Mima, part 2/2: Patterns 5-8 + main function) + Research (TH01's unexpected palette changes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/327730f...454c105\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tOh look, it's another rather short and straightforward boss with a rather\n\tsmall number of bugs and quirks. Yup, contrary to the character's\n\tpopularity, Mima's premiere is really not all that special in terms of code,\n\tand continues the trend established with\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 Kikuri\u003c/a\u003e and\n\t\u003ca href=\"/blog/2022-06-25\"\u003e📝 SinGyoku\u003c/a\u003e. I've already covered\n\t\u003ca href=\"/blog/2021-11-08\"\u003e📝 the initial sprite-related bugs last November\u003c/a\u003e,\n\tso this post focuses on the main code of the fight itself. The overview:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe TH01 Mima fight consists of 3 phases, with phases 1 and 3 each\n\tcorresponding to one half of the 12-HP bar.\n\t\u003ca href=\"/blog/2022-06-25\"\u003e📝 Just like with SinGyoku\u003c/a\u003e, the distinction\n\tbetween the red-white and red parts is purely visual once again, and doesn't\n\treflect anything about the boss script. As usual, all of the phases have to\n\tbe completed in order.\u003c/li\u003e\n\t\u003cli\u003ePhases 1 and 3 cycle through 4 danmaku patterns each, for a total of 8.\n\tThe cycles always start on a fixed pattern.\u003c/li\u003e\n\t\u003cli\u003e3 of the patterns in each phase feature rotating white squares, thus\n\tintroducing a new sprite in need of being unblitted.\u003c/li\u003e\n\t\u003cli\u003ePhase 1 additionally features the \"hop pattern\" as the last one in its\n\tcycle. This is the only pattern where Mima leaves the seal in the center of\n\tthe playfield to hop from one edge of the playfield towards the other, while\n\talso moving slightly higher up on the Y axis, and staying on the final\n\tposition for the next pattern cycle. For the first time, Mima selects a\n\trandom starting edge, which is then alternated on successive cycles.\u003c/li\u003e\n\t\u003cli\u003eSince the square entities are local to the respective pattern function,\n\tPhase 1 can only end once the current pattern is done, even if Mima's HP are\n\talready below 6. This makes Mima susceptible to the\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 test/debug mode HP bar heap corruption bug\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003ePhase 2 simply consists of a spread-in teleport back to Mima's initial\n\tposition in the center of the playfield. This would only have been strictly\n\tnecessary if phase 1 ended on the hop pattern, but is done regardless of the\n\tprevious pattern, and does provide a nice visual separation between the two\n\tmain phases.\u003c/li\u003e\n\t\u003cli\u003eThat's it – nothing special in Phase 3.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAnd there aren't even any weird hitboxes this time. What \u003ci\u003eis\u003c/i\u003e maybe\n\tspecial about Mima, however, is how there's something to cover about all of\n\ther patterns. Since this is TH01, it's won't surprise anyone that the\n\trotating square patterns are one giant copy-pasta of unblitting, updating,\n\tand rendering code. At least ZUN placed the core polar→Cartesian\n\ttransformation in a separate function for creating \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Regular_polygon\"\u003eregular polygons\u003c/a\u003e\n\twith an arbitrary number of sides, which might hint toward some more varied\n\tshapes having been planned at one point?\u003cbr\u003e\n\t5 of the 6 patterns even follow the exact same steps during square update\n\tframes:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eCalculate square corner coordinates\u003c/li\u003e\n\t\u003cli\u003eUnblit the square\u003c/li\u003e\n\t\u003cli\u003eUpdate the square angle and radius\u003c/li\u003e\n\t\u003cli\u003eUse the square corner coordinates for spawning pellets or missiles\u003c/li\u003e\n\t\u003cli\u003eRecalculate square corner coordinates\u003c/li\u003e\n\t\u003cli\u003eRender the square\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tNotice something? Bullets are spawned \u003ci\u003ebefore\u003c/i\u003e the corner coordinates\n\tare updated. That's why their initial positions seem to be a bit off – they\n\t\u003ci\u003eare\u003c/i\u003e spawned exactly in the corners of the square, it's just that it's\n\tthe square from 8 frames ago. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-pattern-0-original.webp?c07165a7\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"389\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-0-original.avi?20da81f1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-pattern-0-original.webm?037254d9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-pattern-0-original.webm?d13a8145\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-pattern-0-original.webm?e5368d23\" type=\"video/webm\"\u003eVideo of TH01-Mima's first pattern on Normal difficulty, in its original version with misaligned pellet spawn positions. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-0-original.avi?20da81f1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"109\" data-title=\"First pellets spawned\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-pattern-0-fixed.webp?c07165a7\" preload=\"none\" controls data-title=\"Dequirked version\" loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"389\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-0-fixed.avi?3bf43138\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-pattern-0-fixed.webm?b7c5cb55\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-pattern-0-fixed.webm?2af9fd5b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-pattern-0-fixed.webm?1c79e29b\" type=\"video/webm\"\u003eVideo of TH01-Mima's first pattern on Normal difficulty, in a fixed version with the pellets spawning exactly on the square corners. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-0-fixed.avi?3bf43138\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"109\" data-title=\"First pellets spawned\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eMima's first pattern on Normal difficulty.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tOnce ZUN reached the final laser pattern though, he must have noticed that\n\tthere's something wrong there… or maybe he just \u003ci\u003ewanted\u003c/i\u003e to fire those\n\tlasers independently from the square unblit/update/render timer for a\n\tchange. Spending an additional 16 bytes of the data segment for conveniently\n\tremembering the square corner coordinates across frames was definitely a\n\tdecent investment.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-pattern-7.webp?90956cc3\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"296\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-7.avi?ac5467b1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-pattern-7.webm?18dd53ed\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-pattern-7.webm?e5375b76\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-pattern-7.webm?3b1bc4a5\" type=\"video/webm\"\u003eVideo of TH01-Mima's laser pattern on Lunatic difficulty, featuring correct laser spawn positions even in the original game. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pattern-7.avi?ac5467b1\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tMima's laser pattern on Lunatic difficulty, now with correct laser spawn\n\t\tpositions. If this pattern reminds you of the game crashing immediately\n\t\twhen defeating Mima,\n\t\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 check out the Elis blog post for the details behind this bug, and grab the bugfix patch from there\u003c/a\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tWhen Mima isn't shooting bullets from the corners of a square or hopping\n\tacross the playfield, she's raising flame pillars \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAQAIABAP8AAMCAACH5BAEKAAEALAAAAAAgABAAAAJEjI8ItgmvYmINsvDanDrn020gBVnlJyqLh3krqm2nS3+WWOd12up+jAH9fo7K0Md5HXWj3pLZEj5zUdWUmry6cFrWoQAAOw==\"\n\t\u003e from the bottom of the playfield within very specifically calculated\n\trandom ranges… which are then rendered at byte-aligned VRAM positions, while\n\tcollision detection still uses their actual pixel position. Since I  don't\n\twant to sound like a broken record all too much, I'll just direct you to\n\t\u003ca href=\"/blog/2022-06-17\"\u003e📝 Kikuri, where we've seen the exact same issue with the teardrop ripple sprites\u003c/a\u003e.\n\tThe conclusions are identical as well.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-pillars-hitboxes.webp?43df6524\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"351\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pillars-hitboxes.avi?910a04d6\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-pillars-hitboxes.webm?131614eb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-pillars-hitboxes.webm?98bda2df\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-pillars-hitboxes.webm?91a1a052\" type=\"video/webm\"\u003eVideo of TH01-Mima's flame pillars, showing the disparity between their internal coordinates and hitboxes, and their byte-aligned rendering. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pillars-hitboxes.avi?910a04d6\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"296\" data-title=\"🌠 Meteor\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"297\" data-title=\"🚫 Cast\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tMima's flame pillar pattern. This video was recorded on a particularly\n\t\tunlucky seed that resulted in great disparities between a pillar's\n\t\tinternal X coordinate and its byte-aligned on-screen appearance, leading\n\t\tto lots of right-shifted hitboxes.\u003cbr\u003e\n\t\tAlso note how the change from the meteor animation to the three-arm 🚫\n\t\tcasting sprite doesn't unblit the meteor, and leaves that job to\n\t\tany sprite that happens to fly over those pixels.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tHowever, I'd say that the saddest part about this pattern is how choppy it\n\tis, with the circle/pillar entities updating and rendering at a meager 7\n\tFPS. Why go that low on purpose when you can just make the game render ✨\n\t\u003ci\u003esmoothly\u003c/i\u003e ✨ instead?\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-pillars-smooth.webp?dba0ea53\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"347\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pillars-smooth.avi?c2d70c54\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-pillars-smooth.webm?ebf55bc1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-pillars-smooth.webm?060192d8\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-pillars-smooth.webm?900f33b4\" type=\"video/webm\"\u003eVideo of TH01-Mima's flame pillar pattern with all circle/pillar entities rendering at a full 56.423 FPS. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-pillars-smooth.avi?c2d70c54\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eSo smooth it's almost uncanny.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe reason quickly becomes obvious: With TH01's lack of optimization, going\n\tfor the full 56.4 FPS would have significantly slowed down the game on its\n\tintended 33 MHz CPUs, requiring more than cheap surface-level ASM\n\toptimization for a stable frame rate. That might very well have been ZUN's\n\treason for only ever rendering one circle per frame to VRAM, and designing\n\tthe pattern with these time offsets in mind. It's always been typical for\n\tPC-98 developers to target the lowest-spec models that could possibly still\n\trun a game, and implementing dynamic frame rates into such an engine-less\n\tgame is nothing I would wish on anybody. And it's not like TH01 is\n\tparticularly unique in its choppiness anyway; low frame rates are actually a\n\trather typical part of the PC-98 game aesthetic.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe final piece of weirdness in this fight can be found in phase 1's hop\n\tpattern, and specifically its palette manipulation. Just from looking at the\n\tpattern code itself, each of the 4 hops is supposed to darken the hardware\n\tpalette by subtracting \u003ccode\u003e#444\u003c/code\u003e from every color. At the last hop,\n\tevery color should have therefore been reduced to a pitch-black\n\t\u003ccode\u003e#000\u003c/code\u003e, leaving the player completely blind to the movement of\n\tthe chasing pellets for 30 frames and making the pattern quite ghostly\n\tindeed. However, that's not what we see in the actual game:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tNothing in the pattern's code would cause the hardware palette to get\n\t\tbrighter before the end of the pattern, and yet…\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThe expected version doesn't look all too unfair, even on Lunatic…\n\t\twell, at least at the default \u003cs\u003erank\u003c/s\u003e pellet speed shown in this\n\t\tvideo. At maximum pellet speed, it \u003ci\u003eis\u003c/i\u003e in fact rather brutal.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-hop-original.webp?2bea4f00\" preload=\"none\" controls data-title=\"Actual palette changes\" loop data-active width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"266\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-hop-original.avi?be48b83b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-hop-original.webm?2494a0b6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-hop-original.webm?dde94e1d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-hop-original.webm?fab45928\" type=\"video/webm\"\u003eVideo of TH01-Mima's hop pattern, in its original version with the external palette resets. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-hop-original.avi?be48b83b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"144\" data-title=\"Last hop\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Mima-hop-expected.webp?2bea4f00\" preload=\"none\" controls data-title=\"Expected palette changes\" loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"266\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-hop-expected.avi?b1c11c4a\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Mima-hop-expected.webm?fa317164\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Mima-hop-expected.webm?eb86355d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Mima-hop-expected.webm?959841ec\" type=\"video/webm\"\u003eVideo of TH01-Mima's hop pattern, with external palette resets removed to reveal the pattern's own palette changes. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Mima-hop-expected.avi?b1c11c4a\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"144\" data-title=\"Last hop\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tLooking at the frame counter, it appears that \u003ci\u003esomething\u003c/i\u003e outside the\n\tpattern resets the palette every 40 frames. The only known constant with a\n\tvalue of 40 would be the invincibility frames after hitting a boss with the\n\tOrb, but we're not hitting Mima here… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tBut as it turns out, that's exactly where the palette reset comes from: The\n\thop animation darkens the hardware palette directly, while the\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 infamous 12-parameter boss collision handler function\u003c/a\u003e\n\tunconditionally resets the hardware palette to the \"default boss palette\"\n\tevery 40 frames, regardless of whether the boss was hit or not. I'd classify\n\tthis as a bug: That function has no business doing periodic hardware palette\n\tresets outside the invincibility flash effect, and it completely defies\n\tcommon sense that it does.\n\u003c/p\u003e\u003cp\u003e\n\tThat explains one unexpected palette change, but could this function\n\tpossibly also explain the other infamous one, namely, the temporary green\n\tdiscoloration in the Konngara fight? That glitch comes down to how the game\n\tactually uses \u003ci\u003etwo\u003c/i\u003e global \"default\" palettes: a default \u003ci\u003eboss\u003c/i\u003e\n\tpalette for undoing the invincibility flash effect, and a default\n\t\u003ci\u003estage\u003c/i\u003e palette for returning the colors back to normal at the end of\n\tthe bomb animation or when leaving the Pause menu. And sure enough, the\n\t\u003ci\u003estage\u003c/i\u003e palette is the one with the green color, while the \u003ci\u003eboss\u003c/i\u003e\n\tpalette contains the intended colors used throughout the fight. Sending the\n\tlatter palette to the graphics chip every 40 frames is what \u003ci\u003ecorrects\u003c/i\u003e\n\tthe discoloration, which would otherwise be permanent.\n\u003c/p\u003e\u003cp\u003e\n\tThe green color comes from \u003ccode\u003eBOSS7_D1.GRP\u003c/code\u003e, the scrolling\n\tbackground of the entrance animation. That's what turns this into a clear\n\tbug: The \u003ci\u003estage\u003c/i\u003e palette is only set a single time in the entire fight,\n\tat the beginning of the entrance animation, to the palette of this image.\n\tApart from consistency reasons, it doesn't even make sense to set the stage\n\tpalette there, as you can't enter the Pause menu or bomb during a blocking\n\tanimation function.\u003cbr\u003e\n\tAnd just 3 lines of code later, ZUN loads \u003ccode\u003eBOSS8_A1.GRP\u003c/code\u003e, the\n\tmain background image of the fight. Moving the stage palette assignment\n\tthere would have easily prevented the discoloration.\n\u003c/p\u003e\u003cp\u003e\n\tBut yeah, as you can tell, palette manipulation is complete jank in this\n\tgame. Why differentiate between a stage and a boss palette to begin with?\n\tThe blocking Pause menu function could have easily copied the original\n\tpalette to a local variable before darkening it, and then restored it after\n\tclosing the menu. It's not so easy for bombs as the intended palette could\n\tchange between the start and end of the animation, but the code could have\n\tstill been simplified a lot if there was just one global \"default palette\"\n\tvariable instead of two. Heck, even the other bosses who manipulate their\n\tpalettes correctly only do so because they manually synchronize the two\n\tafter every change. The proper defense against bugs that result from wild\n\tmutation of global state is to get rid of global state, and not to put up\n\tsafety nets hidden in the middle of existing effect code.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-17-TH01-Konngara-green.webp?09f714f8\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"152\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-07-17-TH01-Konngara-green.avi?65ace5f0\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-17-TH01-Konngara-green.webm?993d5a2e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-17-TH01-Konngara-green.webm?72a745de\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-17-TH01-Konngara-green.webm?0655f5fd\" type=\"video/webm\"\u003eVideo demonstrating the simplest way of reproducing the green discoloration bug during the TH01 Konngara fight. \u003ca href=\"/blog/static/video/zmbv/2022-07-17-TH01-Konngara-green.avi?65ace5f0\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThe easiest way of reproducing the green discoloration bug in\n\tthe TH01 Konngara fight, timed to show the maximum amount of time the\n\tdiscoloration can possibly last.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIn any case, that's Mima done! 7\u003csup\u003eth\u003c/sup\u003e PC-98 Touhou boss fully\n\tdecompiled, 24 bosses remaining, and 59 functions left in all of TH01.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tIn other thrilling news, my call for secondary funding priorities in new\n\tTH01 contributions has given us three different priorities so far. This\n\traises an interesting question though: Which of these contributions should I\n\tnow put towards TH01 immediately, and which ones should I leave in the\n\tbacklog for the time being? Since I've never liked deciding on priorities,\n\tlet's turn this into a popularity contest instead: The contributions with\n\tthe least popular secondary priorities will go towards TH01 first, giving\n\tthe most popular priorities a higher chance to still be left over after TH01\n\tis done. As of this delivery, we'd have the following popularity order:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eTH05 (1.67 pushes), from T0182\u003c/li\u003e\n\t\u003cli\u003eSeihou (1 push), from T0184\u003c/li\u003e\n\t\u003cli\u003eTH03 (0.67 pushes), from T0146\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tWhich means that T0146 will be consumed for TH01 next, followed by T0184 and\n\tthen T0182. I only assign transactions immediately before a delivery though,\n\tso you all still have the chance to change up these priorities before the\n\tnext one.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The final boss of TH01 decompilation, YuugenMagan… \u003cs\u003eif the current\n\tor newly incoming TH01 funds happen to be enough to cover the entire fight.\n\tIf they don't turn out to be, I will have to pass the time with some Seihou\n\twork instead, missing the TH01 anniversary deadline as a result.\u003c/s\u003e\n\t\u003cstrong\u003eEdit (2022-07-18):\u003c/strong\u003e Thanks to Yanga for\n\tsecuring the funding for YuugenMagan after all! That fight will feature\n\tslightly more than half of all remaining code in TH01's\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e and the single biggest function in all of PC-98\n\tTouhou, let's go!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-08-08\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-07-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-07-17T19:20:47Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-07-10",
      "url": "https://rec98.nmlgc.net/blog/2022-07-10",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-07-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-06-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-07-10\"\u003e\u003ctime datetime=\"2022-07-10T11:48:22Z\"\u003e2022-07-10 11:48\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0203\"\u003eP0203\u003c/a\u003e\n\t\t\tTH01 decompilation (Card-flipping stages, part 3/4: Bumpers and turrets)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4568bf7...86cdf5f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0204\"\u003eP0204\u003c/a\u003e\n\t\t\tTH01 decompilation (Card-flipping stages, part 4/4: Portals + Bomb animation)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/86cdf5f...0c682b5\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eGhostRiderCog, [Anonymous], Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/card-flipping\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\n\n\u003cp\u003e\n\tLet's start right with the milestones:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eMore than 50% of all PC-98 Touhou game code has now been\n\treverse-engineered! 🎉 While this number isn't equally distributed among the\n\tgames, we've got one game very close to 100% and reverse-engineered most of\n\tthe core features of two others. During the last 32 months of continuous\n\tfunding, I've averaged an overall speed of 1.11% total RE per month. That\n\tlooks like a decent prediction of how much more time it will take for 100%\n\tacross all games – unless, of course, I'd get to work towards some of the\n\tnon-RE goals in the meantime.\u003c/li\u003e\n\t\u003cli\u003e70 functions left in TH01, with less than 10,000 ASM instructions\n\tremaining! Due to immense hype, I've temporarily raised the cap by 50% until\n\tAugust 15. With the last TH01 pushes delivering at roughly 1.5× of the\n\tcurrently calculated average speed, that should be more than enough to get\n\tTH01 done – especially since I expect YuugenMagan to come with lots of\n\tredundant code. Therefore, please also request a secondary priority for\n\tthese final TH01 RE contributions.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tSo, how did this card-flipping stage obstacle delivery get so horribly\n\tdelayed? With all the different layouts showcased in the 28 card-flipping\n\tstages, you'd expect this to be among the more stable and bug-free parts of\n\tthe codebase. Heck, with all stage objects being placed on a 32×32-pixel\n\tgrid, this is the first TH01-related blog post this year that doesn't have\n\tto describe an alignment-related unblitting glitch!\n\u003c/p\u003e\u003cp\u003e\n\tThat alone doesn't mean that this code is free from quirky behavior though,\n\tand we have to look no further than the first few lines of the collision\n\thandling for round bumpers to already find a whole lot of that. Simplified,\n\tthey do the following:\n\u003c/p\u003e\u003cpre\u003epixel_t delta_y_between_orb_and_bumper = (orb.top - bumper.top);\nif(delta_y_between_orb_and_bumper \u0026lt;= 0) {\n\torb.top = (bumper.top - 24);\n} else {\n\torb.top = (bumper.top + 24);\n}\u003c/pre\u003e\u003cp\u003e\n\tImmediately, you wonder why these assignments only exist for the Y\n\tcoordinate. Sure, hitting a bumper from the left or right side should happen\n\tless often, but it's definitely possible. Is it really a good idea to warp\n\tthe Orb to the top or bottom edge of a bumper regardless?\u003cbr\u003e\n\tWhat's more important though: The fact that these immediate assignments\n\texist at all. The game's regular Orb physics work by producing a Y velocity\n\tfrom the single force acting on the Orb and a gravity factor, and are\n\tcompletely independent of its current Y position. A bumper collision does\n\talso apply a new force onto the Orb further down in the code, but these\n\tassignments still bypass the physics system and are bound to have\n\t\u003ci\u003esome\u003c/i\u003e knock-on effect on the Orb's movement.\n\u003c/p\u003e\u003cp\u003e\n\tTo observe that effect, we just have to enter Stage 18 on the \u003cspan\n\tlang=\"ja\"\u003e地獄\u003c/span\u003e/Jigoku route, where it's particularly trivial to\n\treproduce. At a \u003ca href=\"/blog/2020-06-13\"\u003e📝 horizontal velocity\u003c/a\u003e of ±4,\n\tthese assignments are \u003ci\u003eexactly\u003c/i\u003e what can cause the Orb to endlessly\n\tbounce between two bumpers. As rudimentary as the Orb's physics may be, just\n\tletting them do their work would have entirely prevented these loops:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tOne of at least three infinite bumper loop constellations within just\n\t\tthis 10×5-tile section of TH01's Stage 18 on the \u003cspan\n\t\tlang='ja'\u003e地﻿獄\u003c/span\u003e/Jigoku route. With an effective 56 horizontal\n\t\tpixels between both hitboxes, the Orb would have to travel an absolute\n\t\tY distance of at least 16 vertical pixels within\n\t\t\u003ccode\u003e(56\u0026nbsp;/\u0026nbsp;4)\u0026nbsp;=\u0026nbsp;14\u003c/code\u003e frames to escape the\n\t\tother bumper's hitbox. If the initial bounce reduces the Orb's Y\n\t\tvelocity far enough for it to not manage that distance the first time,\n\t\tit will never reach the necessary speed again. In this loop, the\n\t\tbounce-off force even stabilizes, though this doesn't have to happen.\n\t\tThe blue areas indicate the \u003cspan class='hovertext'\n\t\ttitle=\"Within the usual caveat of the Orb's internal X position not corresponding to its on-screen one, as it's blitted on the byte-aligned 8×1-pixel VRAM grid\"\n\t\u003epixel-perfect*\u003c/span\u003e hitboxes of each bumper.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tTH01 bumper collision handling without ZUN's manual assignment of the Y\n\t\tcoordinate. The Orb still bounces back and forth between two bumpers\n\t\tfor a while, but its \u003ccode\u003etop\u003c/code\u003e position always follows naturally\n\t\tfrom its Y velocity and the force applied to it, and gravity wins out\n\t\tin the end. The blue areas indicate the \u003cspan class='hovertext'\n\t\ttitle=\"Within the usual caveat of the Orb's internal X position not corresponding to its on-screen one, as it's blitted on the byte-aligned 8×1-pixel VRAM grid\"\n\t\u003epixel-perfect*\u003c/span\u003e hitboxes of each bumper.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Bumper-loop-dual-original.webp?75bcc15e\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"320\" data-fps=\"56.423132\" data-frame-count=\"810\" style=\"aspect-ratio: 640 / 320\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-dual-original.avi?0246a044\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Bumper-loop-dual-original.webm?6628f286\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Bumper-loop-dual-original.webm?d49da879\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Bumper-loop-dual-original.webm?8d318854\" type=\"video/webm\"\u003eVideo demonstrating how a collision with TH01's round bumpers immediately teleports the Orb on the Y axis, which can lead to endless loops between two bumpers. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-dual-original.avi?0246a044\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Bumper-loop-dual-no-teleport.webp?75bcc15e\" preload=\"none\" controls data-title=\"Y position assignments removed\" loop width=\"640\" height=\"320\" data-fps=\"56.423132\" data-frame-count=\"478\" style=\"aspect-ratio: 640 / 320\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-dual-no-teleport.avi?5280ff6d\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Bumper-loop-dual-no-teleport.webm?7f23aa1a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Bumper-loop-dual-no-teleport.webm?ccbcdc49\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Bumper-loop-dual-no-teleport.webm?2481cab2\" type=\"video/webm\"\u003eVideo demonstrating how collision with TH01's round bumpers could look with no manual Y assignments, and just the game's usual physics at work. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-dual-no-teleport.avi?5280ff6d\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"256\" data-title=\"Finite bounce between two orbs\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow, you might be thinking that these Y assignments were just an attempt to\n\tprevent the Orb from colliding with the same bumper again on the next frame.\n\tAfter all, those 24 pixels exactly correspond to ⅓ of the height of a\n\tbumper's hitbox with an additional pixel added on top. However, the game\n\talready perfectly prevents repeated collisions by turning off collision\n\ttesting with the same bumper for the next 7 frames after a collision. Thus,\n\twe can conclude that ZUN either explicitly coded bumper collision handling\n\tto facilitate these loops, or just didn't take out that code after\n\tinevitably discovering what it did. This is not janky code, it's not a\n\tglitch, it's not sarcasm from my end, and it's not the game's physics being\n\tbad.\n\u003c/p\u003e\u003cp\u003e\n\tBut wait. Couldn't these assignments just be a remnant from a time in\n\tdevelopment \u003ci\u003ebefore\u003c/i\u003e ZUN decided on the 7-frame delay on further\n\tcollisions? Well, even that explanation stops holding water after the next\n\tfew lines of code. Simplified, again:\n\u003c/p\u003e\u003cpre\u003epixel_t delta_x_between_orb_and_bumper = (orb.left - bumper.left);\nif((orb.velocity.x == +4) \u0026\u0026 (delta_x_between_orb_and_bumper \u0026lt; 0)) {\n\torb.velocity.x = -4;\n} else if((orb.velocity.x == -4) \u0026\u0026 (delta_x_between_orb_and_bumper \u003e 0)) {\n\torb.velocity.x = +4;\n}\n\u003c/pre\u003e\u003cp\u003e\n\tWhat's important here is the part that's \u003ci\u003enot\u003c/i\u003e in the code – namely,\n\tanything that handles X velocities of -8 or +8. In those cases, the Orb\n\tsimply continues in the same horizontal direction. The manual Y assignment\n\tis the only part of the code that actually prevents a collision there, as\n\tthe newly applied force is not guaranteed to be enough:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tAn infinite loop across three bumpers, made possible by the edge of the\n\t\tplayfield and bumper bars on opposite sides, an unchanged horizontal\n\t\tdirection, and the Y assignments neatly placing the Orb on either the\n\t\ttop or bottom side of a bumper. The alternating sign of the force\n\t\tfurther ensures that the Orb will travel upwards half the time,\n\t\tcanceling out gravity during the short time between two hitboxes.\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tWith the unchanged horizontal direction and the Y assignments removed,\n\t\tnothing keeps an Orb at ±8 pixels per frame from flying into/over a\n\t\tbumper. The collision force pushes the Orb slightly, but not enough to\n\t\ttruly matter. The final force sends the Orb on a significant downward\n\t\ttrajectory beyond the next bumper's hitbox, breaking the original loop.\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Bumper-loop-triple-original.webp?30979eee\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"320\" data-fps=\"56.423132\" data-frame-count=\"145\" style=\"aspect-ratio: 640 / 320\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-triple-original.avi?4cc060be\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Bumper-loop-triple-original.webm?0877d76f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Bumper-loop-triple-original.webm?7ed23a1d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Bumper-loop-triple-original.webm?7cfbec3f\" type=\"video/webm\"\u003eVideo demonstrating how TH01's immediate Y assignments on round bumper collisions can even facilitate infinite loops across three bumpers. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-triple-original.avi?4cc060be\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Bumper-loop-triple-no-teleport.webp?751cf7bf\" preload=\"none\" controls data-title=\"Y position assignments removed\" loop width=\"640\" height=\"320\" data-fps=\"56.423132\" data-frame-count=\"84\" style=\"aspect-ratio: 640 / 320\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-triple-no-teleport.avi?9806dcc1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Bumper-loop-triple-no-teleport.webm?60ed51ef\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Bumper-loop-triple-no-teleport.webm?11e4398b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Bumper-loop-triple-no-teleport.webm?8cce9d90\" type=\"video/webm\"\u003eVideo demonstrating how removing TH01's immediate Y assignments on round bumper collisions would affect previously infinite loops across three bumpers. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Bumper-loop-triple-no-teleport.avi?9806dcc1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"60\" data-title=\"Final force\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tForgetting to handle ⅖ of your discrete X velocity cases is simply not\n\tsomething you do by accident. So we might as well say that ZUN deliberately\n\tdesigned the game to behave exactly as it does in this regard.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tBumpers also come in vertical or horizontal bar shapes. Their collision\n\thandling also turns off further collision testing for the next 7 frames, and\n\tdoesn't do any manual coordinate assignment. That's definitely a step up in\n\tcleanliness from round bumpers, but it doesn't seem to keep in mind that the\n\tplayer can fire a new shot every 4 frames when standing still. That makes it\n\timmediately obvious why this works:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.webp?f048c26f\" preload=\"none\" controls loop width=\"640\" height=\"384\" data-fps=\"20\" data-frame-count=\"136\" style=\"aspect-ratio: 640 / 384\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.avi?5b94e691\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.webm?641df78a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.webm?b21bd1aa\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.webm?e787c0a5\" type=\"video/webm\"\u003eVideo demonstrating how TH01's disabling of bumper bar collisions for the next 7 frames after a collision allows the Orb to be mashed through a horizontal bar. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Shooting-Orb-through-horizontal-bar.avi?5b94e691\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe \u003cspan style=\"color:green\"\u003egreen numbers\u003c/span\u003e show the amount of\n\t\tframes since the last detected collision with the respective bumper bar,\n\t\tand indicate that collision testing with the bar below is currently\n\t\tdisabled.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's the most well-known case of reducing the Orb's horizontal velocity to\n\t0 by exactly hitting it with shots in its center and then button-mashing it\n\tthrough a horizontal bar. This also works with vertical bars and yields even\n\tmore interesting results there, but if we want to have any chance of\n\tunderstanding what happens there, we have to first go over some basics:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eCollision detection for all stage obstacles is done in \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Row-_and_column-major_order\"\u003erow-major\n\torder\u003c/a\u003e from the top-left to the bottom-right corner of the\n\tplayfield.\u003c/li\u003e\n\t\u003cli\u003eAll obstacles are collision-tested independently from each other, with\n\tthe collision response code immediately following the test.\u003c/li\u003e\n\t\u003cli\u003eThe hitboxes for bumper bars extend far past their 32×32 sprites to make\n\tsure that the Orb can collide with them from any side. They are a\n\tpixel-perfect* 87×56 pixels for horizontal bars, and 57×87 pixels for\n\tvertical ones. Yes, that's no typo, they really do differ in one pixel.\u003c/li\u003e\n\t\u003cli\u003eChanging the Y velocity during such a collision just involves applying a\n\tnew force with the magnitude of the negated current Y velocity, which can be\n\tdone multiple times during a frame without changing the result. This\n\texplains why the force is correctly inverted in the clip above, despite the\n\tOrb colliding with two bumpers simultaneously.\u003c/li\u003e\n\t\u003cli\u003eLacking a similar force system, the X coordinate is simply directly\n\tinverted.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tHowever, if that were everything the game did, kicking the Orb into a column\n\tof vertical bumper bars would lead them to behave more like a rope that the\n\tOrb can climb, as the initial collision with two hitboxes cancels out the\n\tintended sign change that reflects the Orb away from the bars:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.webp?20835ab4\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"192\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.avi?98cb6400\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.webm?9e092386\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.webm?d155ca9f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.webm?a7487ff4\" type=\"video/webm\"\u003eVideo demonstrating how a column of vertical bumper bars would behave like a climbable rope if ZUN did not add the block flag workaround. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Hypothetical-vertical-bumper-bar-climb.avi?98cb6400\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThis footage was recorded without the workaround I am about to describe.\n\t\tIt does not reflect the behavior of the original game. \u003cstrong\u003eYou\n\t\tcannot do this in the original game.\u003c/strong\u003e\u003cbr\u003e\n\t\tWhile the visualization reveals small sections where three hitboxes\n\t\toverlap, the Orb can never actually collide with three of them at the\n\t\tsame time, as those 3-hitbox regions are 2 pixels smaller than they\n\t\twould need to be to fit the Orb. That's exactly the difference between\n\t\tusing \u003ccode\u003e\u0026lt;\u003c/code\u003e rather than \u003ccode\u003e\u0026lt;=\u003c/code\u003e in these hitbox\n\t\tcomparisons.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhile that would have been a fun gameplay mechanic on its own, it\n\timmediately breaks apart once you place two vertical bumper bars next to\n\teach other. Due to how these bumper bar hitboxes extend past their sprites,\n\tany two adjacent vertical bars will end up with the exact same hitbox in\n\tabsolute screen coordinates. Stage 17 on the\n\t\u003cspan class=\"ja\"\u003e魔﻿界\u003c/span\u003e/Makai route contains exactly such a layout:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.webp?e6bb2dce\" preload=\"none\" controls loop width=\"640\" height=\"512\" data-fps=\"20\" data-frame-count=\"60\" style=\"aspect-ratio: 640 / 512\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.avi?aa1ceca6\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.webm?cad36806\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.webm?52dba7de\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.webm?7ac9428d\" type=\"video/webm\"\u003eVideo demonstrating how two adjacent columns of vertical bumper bars would have canceled out their respective effect if ZUN did not add the block flag workaround. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Two-vertical-bumper-bars-without-blocking.avi?aa1ceca6\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe collision handlers of adjacent vertical bars always activate in the\n\t\tsame frame, independently invert the Orb's X velocity, and therefore\n\t\tfully cancel out their intended effect on the Orb… if the game did not\n\t\thave the workaround I am about to describe. \u003cstrong\u003eThis cannot happen\n\t\tin the original game.\u003c/strong\u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tZUN's workaround: Setting a \"vertical bumper bar block flag\" after any\n\tcollision with such a bar, which simply disables \u003ci\u003eany\u003c/i\u003e collision with\n\t\u003ci\u003eany\u003c/i\u003e vertical bar for the next 7 frames. This quick hack made all\n\tvertical bars work as intended, and avoided the need for involving the Orb's\n\tX velocity in any kind of physics system. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\t\u003cstrong\u003eEdit (2022-07-12):\u003c/strong\u003e This flag only works around glitches\n\tthat would be caused by simultaneously colliding with more than one vertical\n\tbar. The actual response to a bumper bar collision still remains unaffected,\n\tand is \u003ci\u003every\u003c/i\u003e naive:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eHorizontal bars always invert the Orb's Y velocity\u003c/li\u003e\n\t\u003cli\u003eVertical bars invert either the Y or X velocity depending on whether\n\tthe Orb's current X velocity is 0 (Y) or not (X)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThese conditions are only correct if the Orb comes in at an angle roughly\n\tbetween 45° and 135° on either side of a bar. If it's anywhere close to 0°\n\tor 180°, this response \u003ci\u003ewill\u003c/i\u003e be incorrect, and send the Orb straight\n\tthrough the bar. Since the large hitboxes make this easily possible, you can\n\tstill get the Orb to climb a vertical column, or glide along a horizontal\n\trow:\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Real-vertical-bumper-bar-climb.webp?38064a4f\" preload=\"none\" controls loop width=\"256\" height=\"288\" data-fps=\"20\" data-frame-count=\"83\" style=\"aspect-ratio: 256 / 288\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Real-vertical-bumper-bar-climb.avi?51cafed1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Real-vertical-bumper-bar-climb.webm?2c0b494b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Real-vertical-bumper-bar-climb.webm?f40463be\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Real-vertical-bumper-bar-climb.webm?ebde7126\" type=\"video/webm\"\u003eVideo of TH01's Orb actually climbing a column of vertical bumper bars, due the collision response only mirroring the X velocity and doing nothing else. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Real-vertical-bumper-bar-climb.avi?51cafed1\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\n\t\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Horizontal-bumper-bar-glide.webp?6e213b5b\" preload=\"none\" controls loop width=\"768\" height=\"288\" data-fps=\"20\" data-frame-count=\"53\" style=\"aspect-ratio: 768 / 288\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Horizontal-bumper-bar-glide.avi?052432a2\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Horizontal-bumper-bar-glide.webm?823bb122\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Horizontal-bumper-bar-glide.webm?b4ac7519\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Horizontal-bumper-bar-glide.webm?5ab04a36\" type=\"video/webm\"\u003eVideo demonstrating how the naive collision response for horizontal bumper bars in TH01 can lead the Orb to glide along them. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Horizontal-bumper-bar-glide.avi?052432a2\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\n\u003c/figure\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/static/2022-07-10-TH01-Jigoku-Stage-19-bumper-bar-hitbox-overlay.png?fa022511\"\u003eHere's the hitbox overlay for\n\t\u003cspan lang=\"ja\"\u003e地﻿獄\u003c/span\u003e/Jigoku Stage 19\u003c/a\u003e, and here's an updated\n\tversion of the \u003ca href=\"/blog/2020-06-13\"\u003e📝 Orb physics debug mod\u003c/a\u003e that\n\tnow also shows bumper bar collision frame numbers:\n\t\u003ca class=\"download\" href=\"/blog/static/2022-07-10-TH01OrbPhysicsDebug.zip?45bbefb5\" data-kb=\"110.9\"\u003e2022-07-10-TH01OrbPhysicsDebug.zip \u003c/a\u003e\n\tSee the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/th01_orb_debug\"\u003e\u003ccode\u003eth01_orb_debug\u003c/code\u003e\u003c/a\u003e\n\tbranch for the code. To use it, simply replace \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, and\n\trun the game in debug mode, via \u003ckbd\u003egame d\u003c/kbd\u003e on the DOS prompt. If you\n\tencounter a gameplay situation that doesn't seem to be covered by this blog\n\tpost, you can now verify it for yourself. Thanks to \u003ca\n\thref=\"https://touhou-memories.com/\"\u003etouhou-memories\u003c/a\u003e for bringing these\n\tissues to my attention! That definitely was a glaring omission from the\n\tinitial version of this blog post.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith that clarified, we can now try mashing the Orb into these two vertical\n\tbars:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.webp?25435f82\" preload=\"none\" controls loop width=\"640\" height=\"672\" data-fps=\"56.423132\" data-frame-count=\"1129\" style=\"aspect-ratio: 640 / 672\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.avi?30a04bc1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.webm?3e66e94c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.webm?3249680e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.webm?e95f02ba\" type=\"video/webm\"\u003eVideo demonstrating how all collision handling workarounds still allow the Orb to be mashed into two columns of adjacent vertical bumper bars, which end up raising the Orb to the top of the playfield. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Shooting-Orb-through-two-vertical-bars.avi?30a04bc1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"855\" data-title=\"Two collisions within the same frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAt first, that workaround doesn't seem to make a difference here. As we\n\texpect, the frame numbers now tell us that only one of the two bumper bars\n\tin a row activates, but we couldn't have told otherwise as the number of\n\tbars has no effect on newly applied Y velocity forces. On a closer look, the\n\tOrb's rise to the top of the playfield is in fact \u003ci\u003ecaused\u003c/i\u003e by that\n\tworkaround though, combined with the unchanged top-to-bottom order of\n\tcollision testing. As soon as \u003ci\u003eany\u003c/i\u003e bumper bar completed its 7\n\tcollision delay frames, it resets the aforementioned flag, which already\n\treactivates collision handling for any remaining vertical bumper bars during\n\tthe same frame. Look out for frames with both a \u003cspan\n\tstyle=\"color: green;\"\u003e7\u003c/span\u003e and a \u003cspan\n\tstyle=\"color:green;\"\u003e1\u003c/span\u003e, like the one marked in the video above:\n\tThe \u003cspan style=\"color: green;\"\u003e7\u003c/span\u003e will always appear \u003ci\u003ebefore\u003c/i\u003e\n\tthe \u003cspan style=\"color: green;\"\u003e1\u003c/span\u003e in the row-major order. Whenever\n\tthis happens, the current oscillation period is cut down from 7 to 6\n\tframes – and because collision testing runs from top to bottom, this will\n\talways happen during the falling part. Depending on the Y velocity, the\n\trising part may also be cut down to 6 frames from time to time, but that one\n\tat least has a \u003ci\u003echance\u003c/i\u003e to last for the full 7 frames. This difference\n\tadds those crucial extra frames of upward movement, which add up to send the\n\tOrb to the top. Without the flag, you'd always see the Orb oscillating\n\tbetween a fixed range of the bar column.\u003cbr\u003e\n\tFinally, it's the \"top of playfield\" force that gradually slows down the Orb\n\tand makes sure it ultimately only moves at sub-pixel velocities, which have\n\tno visible effect. Because\n\t\u003ca href=\"/blog/2020-06-13\"\u003e📝 the regular effect of gravity\u003c/a\u003e is reset with\n\teach newly applied force, it's completely negated during most of the climb.\n\tThis even holds true once the Orb reached the top: Since the Orb requires a\n\tnegative force to repeatedly arrive up there and be bounced back, this force\n\twill stay active for the first 5 of the 7 collision frames and not move the\n\tOrb at all. Once gravity kicks in at the 5\u003csup\u003eth\u003c/sup\u003e frame and adds 1 to\n\tthe Y velocity, it's already too late: The new velocity can't be larger than\n\t0﻿.﻿5, and the Orb only has 1 or 2 frames before the flag reset causes it to\n\tbe bounced back up to the top again.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tPortals, on the other hand, turn out to be much simpler than the \u003ca\n\thref=\"https://en.touhouwiki.net/index.php?title=Highly_Responsive_to_Prayers%2FGameplay\u0026type=revision\u0026diff=56492\u0026oldid=56491\"\u003eold\n\tdescription that ended up on Touhou Wiki in October 2005\u003c/a\u003e might suggest.\n\tEverything about their teleportations is random: The destination portal, the\n\texit force (as an integer between -9 and +9), as well as the exit X\n\tvelocity, with each of the\n\t\u003ca href=\"/blog/2020-06-13\"\u003e📝 5 distinct horizontal velocities\u003c/a\u003e having an\n\tequal chance of being chosen. Of course, if the destination portal is next\n\tto the left or right edge of the playfield and it chooses to fire the Orb\n\ttowards that edge, it immediately bounces off into the opposite direction,\n\twhereas the 0 velocity is always selected with a constant 20% probability.\n\u003c/p\u003e\u003cp\u003e\n\tThe selection process for the destination portal involves a bit more than a\n\tsingle \u003ccode\u003erand()\u003c/code\u003e call. The game bundles all obstacles in a single\n\tstructure of dynamically allocated arrays, and only knows how many obstacles\n\tthere are \u003ci\u003ein total\u003c/i\u003e, not per type. Now, that alone wouldn't have much\n\tof an impact on random portal selection, as you could simply roll a random\n\tobstacle ID and try again if it's not a portal. But just to be extra cute,\n\tZUN instead iterates over all obstacles, selects any non-entered portal with\n\ta chance of ¼, and just gives up if that dice roll wasn't successful after\n\t16 loops over the whole array, defaulting to the entered portal in that\n\tcase.\u003cbr\u003e\n\tIn all its silliness though, this works perfectly fine, and results in a\n\tchance of 0.75\u003csup\u003e16(𝑛\u0026nbsp;-\u0026nbsp;1)\u003c/sup\u003e for the Orb exiting out of the\n\tsame portal it entered, with 𝑛 being the total number of portals in a\n\tstage. That's 1% for two portals, and 0.01% for three. Pretty decent for a\n\trandom result you don't want to happen, but that hurts nobody if it does.\n\u003c/p\u003e\u003cp\u003e\n\tThe one tiny ZUN bug with portals is technically not even part of the newly\n\tdecompiled code here. If Reimu gets hit while the Orb is being sent through\n\ta portal, the Orb is immediately kicked out of the portal it entered, no\n\tmatter whether it already shows up inside the sprite of the destination\n\tportal. Neither of the two portal sprites is reset when this happens,\n\tleading to \"two Orbs\" being visible simultaneously.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tThis makes very little sense no matter how you look at it. The Orb doesn't\n\treceive a new velocity or force when this happens, so it will simply\n\tre-enter the same portal once the gameplay resumes on Reimu's next life:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-07-10-TH01-Portal-Orb-kickout.webp?acf81238\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"20\" data-frame-count=\"221\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-07-10-TH01-Portal-Orb-kickout.avi?c27f5ff9\"\u003e\u003csource src=\"/blog/static/video/av1/2022-07-10-TH01-Portal-Orb-kickout.webm?eddbdd5b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-07-10-TH01-Portal-Orb-kickout.webm?84bebed7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-07-10-TH01-Portal-Orb-kickout.webm?718b0e99\" type=\"video/webm\"\u003eVideo of TH01, demonstrating how an Orb is kicked out of its entered portal if Reimu gets hit during the teleport animation, only to re-enter it immediately afterwards. \u003ca href=\"/blog/static/video/zmbv/2022-07-10-TH01-Portal-Orb-kickout.avi?c27f5ff9\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"56\" data-title=\"Reimu getting hit\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that's it! At least the turrets don't have anything notable to say about\n\tthem \u003ca href=\"/blog/2020-11-30\"\u003e📝 that I haven't said before\u003c/a\u003e.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThat left another ½ of a push over at the end. Way too much time to finish\n\t\u003ccode\u003eFUUIN.exe\u003c/code\u003e, way too little time to start with Mima… but the bomb\n\tanimation fit perfectly in there. No secrets or bugs there, just a bunch of\n\tsprite animation code wasting at least another 82 bytes in the data segment.\n\tThe special effect after the kuji-in sprites uses the same single-bitplane\n\t32×32 square inversion effect seen at the end of Kikuri's and Sariel's\n\tentrance animation, except that it's a 3-stack of 16-rings moving at 6, 7,\n\tand 8 pixels per frame respectively. At these comparatively slow speeds, the\n\tbyte alignment of each square adds some further noise to the discoloration\n\tpattern… if you even notice it below all the shaking and seizure-inducing\n\thardware palette manipulation.\u003cbr\u003e\n\tAnd yes, due to the very destructive nature of the effect, the game does in\n\tfact rely on it only being applied to VRAM page 0. While that will cause\n\tevery moving sprite to tear holes into the inverted squares along its\n\ttrajectory, keeping a clean playfield on VRAM page 1 is what allows all that\n\tpixel damage to be easily undone at the end of this 89-frame animation.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Mima! Let's hope that stage obstacles already were the most complex\n\tpart remaining in TH01…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-07-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-06-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-07-10T11:48:22Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-06-25",
      "url": "https://rec98.nmlgc.net/blog/2022-06-25",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-07-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-06-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-06-25\"\u003e\u003ctime datetime=\"2022-06-25T16:39:14Z\"\u003e2022-06-25 16:39\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0201\"\u003eP0201\u003c/a\u003e\n\t\t\tTH01 decompilation (SinGyoku, part 1/2: Preparation + sphere movement + patterns 1-2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9342665...ff49e9e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0202\"\u003eP0202\u003c/a\u003e\n\t\t\tTH01 decompilation (SinGyoku, part 2/2: Patterns 3-6 + main function + Missiles, part 2/2 + YuugenMagan setup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ff49e9e...4568bf7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/singyoku\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 5 boss.\"\u003esingyoku\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThe positive:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt only took a record-breaking 1½ pushes to get SinGyoku done!\u003c/li\u003e\n\t\u003cli\u003eNo \u003ca href=\"/blog/2022-05-31\"\u003e📝 entity synchronization code\u003c/a\u003e after\n\tall! Since all of SinGyoku's sprites are 96×96 pixels, ZUN made the rather\n\tsmart decision of just using the sphere entity's position to render the\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 flash and person entities\u003c/a\u003e – and their only\n\tappearance is encapsulated in a single sphere→person→sphere transformation\n\tfunction.\u003c/li\u003e\n\t\u003cli\u003eJust like Kikuri, SinGyoku's code as a whole is not a complete\n\tdisaster.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe negative:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIt's still exactly as buggy as Kikuri, with both of the ZUN bugs being\n\trendering glitches in a single function once again.\u003c/li\u003e\n\t\u003cli\u003eIt also happens to come with a weird hitbox, …\u003c/li\u003e\n\t\u003cli\u003e… and some minor questionable and weird pieces of code.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe overview:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSinGyoku's fight consists of 2 phases, with the first one corresponding\n\tto the white part from 8 to 6 HP, and the second one to the rest of the HP\n\tbar. The distinction between the red-white and red parts is purely visual,\n\tand doesn't reflect anything about the boss script.\u003c/li\u003e\n\t\u003cli\u003eBoth phases cycle between a pellet pattern and SinGyoku's sphere form\n\tslamming itself into the player, followed by it slightly overshooting its\n\tintended base Y position on its way back up.\u003c/li\u003e\n\t\u003cli\u003ePhase 1 only consists of the sphere form's half-circle spray pattern.\n\tTechnically, the phase can only end \u003ci\u003eduring\u003c/i\u003e that pattern, but adding\n\tthat one additional condition to allow it to end during the slam+return\n\t\"pattern\" wouldn't have made a difference anyway. The code doesn't rule out\n\tnegative HP during the slam (have fun in test or debug mode), but the sum of\n\tinvincibility frames alone makes it impossible to hit SinGyoku 7 times\n\tduring a single slam in regular gameplay.\u003c/li\u003e\n\t\u003cli\u003ePhase 2 features two patterns for both the female and male forms\n\trespectively, which are selected randomly.\u003c/li\u003e\n\t\u003cli\u003eThat's it – no hidden timeouts nor \u003ca href=\"/blog/2022-05-31\"\u003e📝 test/debug mode heap corruption susceptibility\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tThis time, we're back to the Orb hitbox being a logical 49×49 pixels in\n\tSinGyoku's center, and the shot hitbox being the weird one. What happens if\n\tyou want the shot hitbox to be \u003ci\u003eboth\u003c/i\u003e offset to the left a bit\n\t\u003ci\u003eand\u003c/i\u003e stretch the entire width of SinGyoku's sprite? You get a hitbox\n\tthat ends in mid-air, far away from the right edge of the sprite:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-25-TH01-SinGyoku-shot-hitbox.webp?155f2144\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"244\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-06-25-TH01-SinGyoku-shot-hitbox.avi?e223f442\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-25-TH01-SinGyoku-shot-hitbox.webm?8aef03f2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-25-TH01-SinGyoku-shot-hitbox.webm?fa60d960\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-25-TH01-SinGyoku-shot-hitbox.webm?414ffed4\" type=\"video/webm\"\u003eVideo of TH01 SinGyoku's weirdly aligned hitbox against player shots. \u003ca href=\"/blog/static/video/zmbv/2022-06-25-TH01-SinGyoku-shot-hitbox.avi?e223f442\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"16\" data-title=\"\u003ccode\u003egx = 376\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"176\" data-title=\"\u003ccode\u003egx = 380\u003c/code\u003e\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tDue to VRAM byte alignment, all player shots fired between\n\t\t\u003ccode\u003egx\u0026nbsp;= 376\u003c/code\u003e and \u003ccode\u003egx\u0026nbsp;= 383\u003c/code\u003e inclusive\n\t\tappear at the same visual X position, but are internally already partly\n\t\toutside the hitbox and therefore won't hit SinGyoku – compare the\n\t\tmarked shot at \u003ccode\u003egx\u0026nbsp;= 376\u003c/code\u003e to the one at \u003ccode\u003egx\u0026nbsp;=\n\t\t380\u003c/code\u003e. So much for precisely visualizing hitboxes in this game…\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSince the female and male forms also use the sphere entity's coordinates,\n\tthey share the same hitbox.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOnto the rendering glitches then, which can – you guessed it – all be found\n\tin the sphere form's slam movement:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eZUN unblits the delta area between the sphere's previous and current\n\tposition on every frame, but reblits the sphere itself on… only every second\n\tframe?\u003c/li\u003e\n\t\u003cli\u003eFor negative X velocities, ZUN made a typo and subtracted the Y velocity\n\tfrom the right edge of the area to be unblitted, rather than adding the X\n\tvelocity. On a cursory look, this shouldn't affect the game all \u003ci\u003etoo\u003c/i\u003e\n\tmuch due to the unblitting function's word alignment. Except when it does:\n\tIf the Y velocity is much smaller than the X one, the left edge of the\n\tunblitted area can, on certain frames, easily align to a word address past\n\tthe previous right edge of the sphere. As a result, not a single sphere\n\tpixel will actually be unblitted, and a small stripe of the sphere will be\n\tleft in VRAM for one frame, until the alignment has caught up with the\n\tsphere's movement in the next one.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.webp?d25300dd\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"166\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.avi?2da1f931\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.webm?46f87ed0\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.webm?0f749156\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.webm?84d20db0\" type=\"video/webm\"\u003eVideo demonstrating both rendering glitches during TH01 SinGyoku's sphere movement. \u003ca href=\"/blog/static/video/zmbv/2022-06-25-TH01-SinGyoku-sphere-slam-unblitting.avi?2da1f931\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"5\" data-title=\"Lazy reblitting\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"36\" data-title=\"Broken unblitting\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"94\" data-title=\"Reimu disappears\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tBy having the sphere move from the right edge of the playfield to the\n\t\tleft, this video demonstrates both the lazy reblitting and broken\n\t\tunblitting at the right edge for negative X velocities. Also, isn't it\n\t\tfunny how Reimu can partly disappear from all the sloppy\n\t\tSinGyoku-related unblitting going on after her sprite was blitted?\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tDue to the low contrast of the sphere against the background, you typically\n\tdon't notice these glitches, but the white invincibility flashing after a\n\thit really does draw attention to them. This time, all of these glitches\n\taren't even directly \u003ci\u003ecaused\u003c/i\u003e by ZUN having never learned about the\n\tEGC's bit length register – if he just wrote correct code for SinGyoku, none\n\tof this would have been an issue. Sigh… I wonder how many more glitches will\n\tbe caused by improper use of this one function in the last 18% of\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tThere's even another bug here, with ZUN hardcoding a horizontal delta of 8\n\tpixels rather than just passing the actual X velocity. Luckily, the maximum\n\tmovement speed is 6 pixels on Lunatic, and this would have only turned into\n\tan additional observable glitch if the X velocity were to exceed 24 pixels.\n\tBut that just means it's the kind of bug that still drains RE attention to\n\tprove that you \u003ci\u003ecan't\u003c/i\u003e actually observe it in-game under \u003ci\u003esome\u003c/i\u003e\n\tcircumstances.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe 5 pellet patterns are all pretty straightforward, with nothing to talk\n\tabout. The code architecture during phase 2 does hint towards ZUN having had\n\tmore creative patterns in mind – especially for the male form, which uses\n\tthe transformation function's three pattern callback slots for three\n\trepetitions of the same pellet group.\u003cbr\u003e\n\tThere is one more oddity to be found at the very end of the fight:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003cimg src=\"/blog/static/2022-06-25-TH01-SinGyoku-defeat.png?0f533385\" alt=\"The first frame of TH01 SinGyoku's defeat animation, showing the sphere blitted on top of a potentially active person form\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tRight before the defeat white-out animation, the sphere form is explicitly\n\treblitted for no reason, on top of the form that was blitted to VRAM in the\n\tprevious frame, and regardless of which form is currently active. If\n\tSinGyoku was meant to immediately transform back to the sphere form before\n\tbeing defeated, why isn't the person form unblitted before then? Therefore,\n\tthe visibility of both forms is undeniably canon, and there is \u003ci\u003esome\u003c/i\u003e\n\tlore meaning to be found here… \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tIn any case, that's SinGyoku done! 6\u003csup\u003eth\u003c/sup\u003e PC-98 Touhou boss fully\n\tdecompiled, 25 remaining.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNo \u003ccode\u003eFUUIN.EXE\u003c/code\u003e code rounding out the last push for a change, as\n\tthe \u003ca href=\"/blog/2021-11-08\"\u003e📝 remaining missile code\u003c/a\u003e has been\n\twaiting in front of SinGyoku for a while. It already looked bad in November,\n\tbut the angle-based sprite selection function definitely takes the cake when\n\tit comes to unnecessary and decadent floating-point abuse in this game.\n\t\u003cbr\u003e\n\tThe algorithm itself is very trivial: Even with\n\t\u003ca href=\"/blog/2020-07-27\"\u003e📝 .PTN requiring an additional \u003ccode\u003equarter\u003c/code\u003e parameter to access 16×16 sprites\u003c/a\u003e,\n\tit's essentially just one bit shift, one addition, and one binary\n\t\u003ccode\u003eAND\u003c/code\u003e. For whatever reason though, ZUN casts the 8-bit missile\n\tangle into a 64-bit \u003ccode\u003edouble\u003c/code\u003e, which turns the following explicit\n\tcomparisons (!) against all possible \u003cspan class=\"hovertext\"\n\ttitle=\"32×32 sprite\"\u003e4\u003c/span\u003e + \u003cspan class=\"hovertext\"\n\ttitle=\"16×16 quarter inside the 32×32 sprite\"\u003e16\u003c/span\u003e boundary angles (!!)\n\tinto FPU operations. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Even with naive and readable\n\tdivision and modulo operations, and the whole existence of this function not\n\tplaying well with Turbo C++ 4.0J's terrible code generation at all, this\n\tcould have been 3 lines of code and 35 un-inlined constant-time\n\tinstructions. Instead, we've got this 207-instruction monster… but hey, at\n\tleast it works. 🤷\u003cbr\u003e\n\tThe remaining time then went to YuugenMagan's initialization code, which\n\tallowed me to immediately remove more declarations from ASM land, but more\n\ton that once we get to the rest of that boss fight.\n\u003c/p\u003e\u003cp\u003e\n\tThat leaves 76 functions until we're done with TH01! Next up: Card-flipping\n\tstage obstacles.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-07-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-06-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-06-25T16:39:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-06-17",
      "url": "https://rec98.nmlgc.net/blog/2022-06-17",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-06-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-05-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-06-17\"\u003e\u003ctime datetime=\"2022-06-17T17:03:14Z\"\u003e2022-06-17 17:03\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0198\"\u003eP0198\u003c/a\u003e\n\t\t\tTH01 decompilation (Kikuri, part 1/3: Preparation + soul, tear, and ripple animations)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/48db0b7...440637e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0199\"\u003eP0199\u003c/a\u003e\n\t\t\tTH01 decompilation (Kikuri, part 2/3: Patterns)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/440637e...5af2048\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0200\"\u003eP0200\u003c/a\u003e\n\t\t\tTH01 decompilation (Kikuri, part 3/3: Main function + Ending boss slideshow + Good/Bad endings)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5af2048...67e46b5\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, \u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kikuri\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 地獄/Jigoku route.\"\u003ekikuri\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/performance\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Evidence-based observations about programming a PC-98 with performance in mind. Does not include ramblings that aren\u0026#39;t substantiated with measurements.\"\u003eperformance\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWhat's this? A simple, straightforward, easy-to-decompile TH01 boss with\n\tjust a few minor quirks and only two rendering-related ZUN bugs? Yup, 2½\n\tpushes, and Kikuri was done. Let's get right into the overview:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eJust like \u003ca href=\"/blog/2022-05-31\"\u003e📝 Elis\u003c/a\u003e, Kikuri's fight consists\n\tof 5 phases, excluding the entrance animation. For some reason though, they\n\tare numbered from 2 to 6 this time, skipping phase 1? For consistency, I'll\n\tuse the original phase numbers from the source code in this blog post.\u003c/li\u003e\n\t\u003cli\u003eThe main phases (2, 5, and 6) also share Elis' HP boundaries of 10, 6,\n\tand 0, respectively, and are once again indicated by different colors in the\n\tHP bar. They immediately end upon reaching the given number of HP, making\n\tKikuri immune to the\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 heap corruption in test or debug mode that can happen with Elis and Konngara\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003ePhase 2 solely consists of the infamous big symmetric spiral\n\tpattern.\u003c/li\u003e\n\t\u003cli\u003ePhase 3 fades Kikuri's ball of light from its default \u003cspan\n\tstyle=\"color: #bbbbff\"\u003ebluish color\u003c/span\u003e to \u003cspan style=\"color:\n\t#ffbbaa\"\u003ebronze\u003c/span\u003e over 100 frames. Collision detection is deactivated\n\tduring this phase.\u003c/li\u003e\n\t\u003cli\u003eIn Phase 4, Kikuri activates her two souls while shooting the spinning\n\t8-pellet circles from the previously activated ball. The phase ends shortly\n\tafter the souls fired their third spread pellet group.\u003cul\u003e\n\t\t\u003cli\u003eNote that this is a timed phase without an HP boundary, which makes\n\t\tit possible to reduce Kikuri's HP below the boundaries of the next\n\t\tphases, effectively skipping them. Take \u003ca\n\t\thref=\"https://youtu.be/gh20KvvEgMM?t=39\"\u003ethis video\u003c/a\u003e for example,\n\t\twhere Kikuri has 6 HP by the end of Phase 4, and therefore directly\n\t\tstarts Phase 6.\u003cbr\u003e\n\t\t(Obviously, Kikuri's HP can also be reduced to 0 or below, which will\n\t\tend the fight immediately after this phase.)\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003ePhase 5 combines the teardrop/ripple \"pattern\" from the souls with the\n\t\"two crossed eye laser\" pattern, on independent cycles.\u003c/li\u003e\n\t\u003cli\u003eFinally, Kikuri cycles through her remaining 4 patterns in Phase 6,\n\twhile the souls contribute single aimed pellets every 200 frames.\u003c/li\u003e\n\t\u003cli\u003eInterestingly, all HP-bounded phases come with an additional hidden\n\ttimeout condition:\u003cul\u003e\n\t\t\u003cli\u003ePhase 2 automatically ends after 6 cycles of the spiral pattern, or\n\t\t5,400 frames in total.\u003c/li\u003e\n\t\t\u003cli\u003ePhase 5 ends after 1,600 frames, or the first frame of the\n\t\t7\u003csup\u003eth\u003c/sup\u003e cycle of the two crossed red lasers.\u003c/li\u003e\n\t\t\u003cli\u003eIf you manage to keep Kikuri alive for 29 of her Phase 6 patterns,\n\t\ther HP are automatically set to 1. The HP bar isn't redrawn when this\n\t\thappens, so there is no visual indication of this timeout condition even\n\t\texisting – apart from the next Orb hit ending the fight regardless of\n\t\tthe displayed HP. Due to the deterministic order of patterns, this\n\t\talways happens on the 8\u003csup\u003eth\u003c/sup\u003e cycle of the \"symmetric gravity\n\t\tpellet lines from both souls\" pattern, or 11,800 frames. If dodging and\n\t\tavoiding orb hits for 3½ minutes sounds tiring, you can always watch the\n\t\tbyte at \u003ccode\u003eDS:0x1376\u003c/code\u003e in your emulator's memory viewer. Once\n\t\tit's at \u003ccode\u003e0x1E\u003c/code\u003e, you've reached this timeout.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo yeah, there's your new timeout challenge. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe few issues in this fight all relate to hitboxes, starting with the main\n\tone of Kikuri against the Orb. The coordinates in the code clearly describe\n\ta hitbox in the upper center of the disc, but then ZUN wrote a \u0026lt; sign\n\tinstead of a \u0026gt; sign, resulting in an in-game hitbox that's not\n\t\u003ci\u003equite\u003c/i\u003e where it was intended to be…\n\u003c/p\u003e\u003cfigure class=\"pixelated th01_playfield\"\u003e\n\t\u003cfigcaption\u003e\n\t\t\n\t\t\n\t\t\u003cbutton id=\"2022-06-17-show-intended\" onclick=\"\n\t\t\tdocument.getElementById('2022-06-17-actual').classList.remove('active');\n\t\t\tdocument.getElementById('2022-06-17-intended').classList.add('active');\n\t\t\tdocument.getElementById('2022-06-17-show-intended').hidden = true;\n\t\t\tdocument.getElementById('2022-06-17-show-actual').hidden = false;\n\t\t\tdocument.getElementById('2022-06-17-hitbox-caption').innerHTML = \u0026#34;Kikuri\u0026#39;s intended hitbox.\u0026#34;;\n\t\t\"\u003e(Show intended hitbox)\u003c/button\u003e\n\t\t\u003cbutton id=\"2022-06-17-show-actual\" onclick=\"\n\t\t\tdocument.getElementById('2022-06-17-actual').classList.add('active');\n\t\t\tdocument.getElementById('2022-06-17-intended').classList.remove('active');\n\t\t\tdocument.getElementById('2022-06-17-show-actual').hidden = true;\n\t\t\tdocument.getElementById('2022-06-17-show-intended').hidden = false;\n\t\t\tdocument.getElementById('2022-06-17-hitbox-caption').innerHTML = \u0026#34;Kikuri\u0026#39;s actual hitbox.\u0026#34;;\n\t\t\" hidden\u003e(Show actual hitbox)\u003c/button\u003e\n\t\t\u003cspan id=\"2022-06-17-hitbox-caption\"\u003eKikuri\u0026#39;s actual hitbox.\u003c/span\u003e\n\t\tSince the Orb sprite doesn't change its shape, we can visualize the\n\t\thitbox in a pixel-perfect way here. The Orb must be completely within\n\t\tthe \u003cspan style=\"color: red\"\u003ered area\u003c/span\u003e for a hit to be registered.\n\t\u003c/figcaption\u003e\n\t\u003cdiv class=\"multilayer\"\u003e\n\t\t\n\t\t\u003cimg src=\"/blog/static/2022-06-17-TH01-Kikuri-playfield.png?0bddeaa4\" class=\"active\" alt=\"TODO\"\u003e\n\t\t\u003cimg\n\t\t\tid=\"2022-06-17-intended\"\n\t\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAFQAQMAAAAV1V3SAAAABlBMVEVpRwDwAADVixDUAAAAAXRSTlMAQObYZgAAAJpJREFUeJzt2LENAjEURMFFBISUQKlQGqVQAgUgTHSSTeqP8EkzBbxoo00AAAAAAGDPTm3TWrvP1pJc+uBztpbk2gdfs7Ukfa+9Z2vJYQi22VxyHIO31YJfq6nYTXUw5zH4EBQUFBQUFBQUFBQUFBQUFFwyuPrNsoerqvqdq/4Pf/FwVn+w1S8xAAAAAAAAAAAAAAAAAADA33wA8BYPrD97Uz8AAAAASUVORK5CYII=\"\n\t\t\tstyle=\"opacity: 0.5\"\n\t\t\talt=\"TH01 Kikuri's intended hitbox\"\n\t\t\u003e\u003cimg\n\t\t\tid=\"2022-06-17-actual\"\n\t\t\tclass=\"active\"\n\t\t\tsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAFQAQMAAAAV1V3SAAAABlBMVEVARhnwAADlWXWPAAAAAXRSTlMAQObYZgAAAIdJREFUeNrt0rENg1AQBcFnOSCkBJdqSqMUSqAAyyblSBxc8pFmCthokz/mX7GlS1BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFDwbsFMNbiOFkyeNbiMFkweNdjNJSm9b7eW5H0Ofrq1JK9zcO/Wrt90rwEAAAAAAAAAAAAAAAAAAAAAAACGcgD8KH1Pn8wO8QAAAABJRU5ErkJggg==\"\n\t\t\tstyle=\"opacity: 0.5\"\n\t\t\talt=\"TH01 Kikuri's actual hitbox\"\n\t\t\u003e\n\t\u003c/div\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tMuch worse, however, are the teardrop ripples. It already starts with their\n\trendering routine, which places the sprites from \u003ccode\u003eTAMAYEN.PTN\u003c/code\u003e\n\t\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAQAKEDAMAwoKDQ8PDw8P///yH5BAEKAAMALAAAAAAgABAAAAJlnI9pYXI+mJp0OCZzrQ5aC4Wbgn1BKWgjEqRpdGLtux7xmcYpENcHD7zFeAGAzwAEBJPK4ufRADWU1KpVCY0sYJ+rl6p5uXDIr7clTA/N1xtRfWK3h/SivSiv2sH8e/5vhQcIUAAAOw==\"\n\t\u003e at byte-aligned VRAM positions in the ultimate piece of \u003ccode\u003eif(…) {…}\n\telse\u0026nbsp;if(…) {…} else\u0026nbsp;if(…) {…}\u003c/code\u003e meme code. Rather than\n\ttracking the position of each of the five ripple sprites, ZUN suddenly went\n\tpurely functional and manually hardcoded the exact rendering and collision\n\tdetection calls for each frame of the animation, based on nothing but its\n\ttotal frame counter. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tEach of the (up to) 5 columns is also unblitted and blitted individually\n\tbefore moving to the next column, starting at the center and then\n\tsymmetrically moving out to the left and right edges. This wouldn't be a\n\tproblem \u003ci\u003eif ZUN's EGC-powered unblitting function didn't word-align its X\n\tcoordinates to a 16×1 grid\u003c/i\u003e. If the ripple sprites happen to start at an\n\todd VRAM byte position, their unblitting coordinates get rounded both down\n\tand up to the nearest 16 pixels, thus touching the adjacent 8 pixels of the\n\tpreviously blitted columns and leaving the well-known black vertical bars in\n\ttheir place. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tOK, so where's the hitbox issue here? If you just look at the raw\n\tcalculation, it's a slightly confusingly expressed, but perfectly logical 17\n\tpixels. But this is where byte-aligned blitting has a direct effect on\n\tgameplay: These ripples can be spawned at any arbitrary, non-byte-aligned\n\tVRAM position, and collisions are calculated relative to this internal\n\tposition. Therefore, the actual hitbox is shifted up to 7 pixels to the\n\tright, compared to where you would expect it from a ripple sprite's\n\ton-screen position:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Kikuri-Ripple-hitboxes.webp?e2368110\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"147\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-Ripple-hitboxes.avi?39b3c78a\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Kikuri-Ripple-hitboxes.webm?714898a9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Kikuri-Ripple-hitboxes.webm?1ccddcc6\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Kikuri-Ripple-hitboxes.webm?12f88ed4\" type=\"video/webm\"\u003eVideo showing the disparity between the byte-aligned rendering of TH01 Kikuri's teardrop ripples, and their internal coordinate. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-Ripple-hitboxes.avi?39b3c78a\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eDue to the deterministic nature of this part of the fight, it's\n\talways 5 pixels for this first set of ripples. These visualizations are\n\tobviously not pixel-perfect due to the different potential shapes of\n\tReimu's sprite, so they instead relate to her 32×32 bounding box, which\n\tneeds to be entirely inside the \u003cspan style=\"color: red\"\u003ered\n\tarea\u003c/span\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWe've previously seen the same issue with the\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 shot hitbox of Elis' bat form\u003c/a\u003e, where\n\tpixel-perfect collision detection against a byte-aligned sprite was merely a\n\tsidenote compared to the more serious X=Y coordinate bug. So why do I\n\televate it to bug status here? Because it directly affects dodging: Reimu's\n\tregular movement speed is 4 pixels per frame, and with the internal position\n\tof an on-screen ripple sprite varying by up to 7 pixels, any micrododging\n\t(or \"grazing\") attempt turns into a coin flip. It's \u003ci\u003esort of\u003c/i\u003e mitigated\n\tby the fact that Reimu is \u003ci\u003ealso\u003c/i\u003e only ever rendered at byte-aligned\n\tVRAM positions, but I wouldn't say that these two bugs cancel out each\n\tother.\u003cbr\u003e\n\tOh well, another set of rendering issues to be fixed in the hypothetical\n\tAnniversary Edition – obviously, the hitboxes should remain unchanged. Until\n\tthen, you can always memorize the exact internal positions. The sequence of\n\tteardrop spawn points is completely deterministic and only controlled by the\n\tfixed per-difficulty spawn interval.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAside from more minor coordinate inaccuracies, there's not much of interest\n\tin the rest of the pattern code. In another parallel to Elis though, the\n\tfirst soul pattern in phase 4 is aimed on every difficulty \u003ci\u003eexcept\u003c/i\u003e\n\tLunatic, where the pellets are once again statically fired downwards. This\n\ttime, however, the pattern's difficulty is much more appropriately\n\tdistributed across the four levels, with the simultaneous spinning circle\n\tpellets adding a constant aimed component to every difficulty level.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Kikuri-phase-4-Easy.webp?d034225b\" preload=\"none\" controls data-title=\"Easy\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"340\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Easy.avi?160410f8\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Kikuri-phase-4-Easy.webm?4e10bee7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Kikuri-phase-4-Easy.webm?6d293a20\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Kikuri-phase-4-Easy.webm?31c73dca\" type=\"video/webm\"\u003eVideo of the phase 4 patterns in the TH01 Kikuri fight, on Easy Mode. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Easy.avi?160410f8\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Kikuri-phase-4-Normal.webp?fd56512c\" preload=\"none\" controls data-title=\"Normal\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"340\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Normal.avi?b74cc718\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Kikuri-phase-4-Normal.webm?65aed818\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Kikuri-phase-4-Normal.webm?52d10c7a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Kikuri-phase-4-Normal.webm?a4b61c00\" type=\"video/webm\"\u003eVideo of the phase 4 patterns in the TH01 Kikuri fight, on Normal Mode. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Normal.avi?b74cc718\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Kikuri-phase-4-Hard.webp?35ec964e\" preload=\"none\" controls data-title=\"Hard\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"340\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Hard.avi?1da7ac2f\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Kikuri-phase-4-Hard.webm?e2cb7026\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Kikuri-phase-4-Hard.webm?6aaf1f7f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Kikuri-phase-4-Hard.webm?cf35d4d7\" type=\"video/webm\"\u003eVideo of the phase 4 patterns in the TH01 Kikuri fight, on Hard Mode. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Hard.avi?1da7ac2f\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Kikuri-phase-4-Lunatic.webp?4e193821\" preload=\"none\" controls data-title=\"Lunatic\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"340\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Lunatic.avi?b45444e6\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Kikuri-phase-4-Lunatic.webm?40daabb4\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Kikuri-phase-4-Lunatic.webm?21d65edf\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Kikuri-phase-4-Lunatic.webm?7f011442\" type=\"video/webm\"\u003eVideo of the phase 4 patterns in the TH01 Kikuri fight, on Lunatic Mode. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Kikuri-phase-4-Lunatic.avi?b45444e6\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eKikuri's phase 4 patterns, on every difficulty.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tThat brings us to 5 fully decompiled PC-98 Touhou bosses, with 26 remaining…\n\tand another ½ of a push going to the cutscene code in\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e.\u003cbr\u003e\n\tYou wouldn't expect something as mundane as the boss slideshow code to\n\tcontain anything interesting, but there is in fact a slight bit of\n\tspeculation fuel there. The text typing functions take explicit string\n\tlengths, which precisely match the corresponding strings… for the most part.\n\tFor the \u003ccode\u003e\"Gatekeeper 'SinGyoku'\"\u003c/code\u003e string though, ZUN passed 23\n\tcharacters, not 22. Could that have been the \"h\" from the Hepburn\n\tromanization of \u003cspan lang=\"ja\"\u003e神玉\u003c/span\u003e?!\u003cbr\u003e\n\tAlso, \u003ci\u003ecome on\u003c/i\u003e, if this text is already blitted to VRAM for no reason,\n\tyou could have gone for perfect centering at unaligned byte positions; the\n\trendering function would have perfectly supported it. Instead, the X\n\tcoordinates are still rounded up to the nearest byte.\n\u003c/p\u003e\u003cp\u003e\n\tThe hardcoded ending cutscene functions should be even less interesting –\n\tdon't they just show a bunch of images followed by frame delays? Until they\n\tdon't, and we reach the \u003cspan lang=\"ja\"\u003e地獄\u003c/span\u003e/Jigoku Bad Ending with\n\tits special shake/\"boom\" effect, and this picture:\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003cimg src=\"/blog/static/2022-06-17-TH01-ED2A.GRP-2.png?2190efb3\"\u003e\n\t\u003cfigcaption\u003ePicture #2 from \u003ccode\u003eED2A.GRP\u003c/code\u003e.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhich is rendered by the following code:\n\u003c/p\u003e\u003cpre\u003efor(int i = 0; i \u0026lt;= boom_duration; i++) { // (yes, off-by-one)\n\tif((i \u0026 3) == 0) {\n\t\tgraph_scrollup(8);\n\t} else {\n\t\tgraph_scrollup(0);\n\t}\n\n\tend_pic_show(1); // ← different picture is rendered\n\tframe_delay(2);  // ← blocks until 2 VSync interrupts have occurred\n\n\tif(i \u0026 1) {\n\t\tend_pic_show(2); // ← picture above is rendered\n\t} else {\n\t\tend_pic_show(1);\n\t}\n}\u003c/pre\u003e\u003cp\u003e\n\tNotice something? \u003ci\u003eYou should never see this picture because it's\n\timmediately overwritten before the frame is supposed to end.\u003c/i\u003e And yet\n\tit's clearly flickering up for about one frame with common emulation\n\tsettings as well as on my real PC-9821 Nw133, clocked at 133 MHz.\n\tmaster.lib's \u003ccode\u003egraph_scrollup()\u003c/code\u003e doesn't block until VSync either,\n\tand removing these calls doesn't change anything about the blitted images.\n\t\u003ccode\u003eend_pic_show()\u003c/code\u003e uses the EGC to blit the given 320×200 quarter\n\tof VRAM from page 1 to the visible page 0, so the bottleneck shouldn't be\n\tthere either…\n\u003c/p\u003e\u003cp\u003e\n\t…or should it? After setting it up via a few I/O port writes, the common\n\tmethod of EGC-powered blitting works like this:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eRead 16 bits from the source VRAM position on \u003ci\u003eany single\u003c/i\u003e\n\tbitplane. This fills the EGC's 4 16-bit tile registers with the VRAM\n\tcontents at that specific position on \u003ci\u003eevery\u003c/i\u003e bitplane. You do not care\n\tabout the value the CPU returns from the read – in optimized code, you would\n\tmake sure to just read into a register to avoid useless additional stores\n\tinto local variables.\u003c/li\u003e\n\t\u003cli\u003eWrite \u003cspan class=\"hovertext\" title=\"Yes, it doesn't even have to be the value you got back during the previous read.\"\u003e\u003ci\u003eany\u003c/i\u003e 16 bits\u003c/span\u003e\n\tto the target VRAM position on \u003ci\u003eany single\u003c/i\u003e bitplane. This copies the\n\tcontents of the EGC's tile registers to that specific position on\n\t\u003ci\u003eevery\u003c/i\u003e bitplane.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tTo transfer pixels from one VRAM page to another, you insert an additional\n\twrite to I/O port \u003ccode\u003e0xA6\u003c/code\u003e before 1) and 2) to set your source and\n\tdestination page… and that's where we find the bottleneck. Taking a look at\n\tthe i486 CPU and its \u003ca\n\thref=\"https://www2.math.uni-wuppertal.de/~fpf/Uebungen/GdR-SS02/opcode_i.html\"\u003ecycle\n\tcounts\u003c/a\u003e, a single one of these page switches costs 17 cycles – 1 for\n\t\u003ccode\u003eMOV\u003c/code\u003eing the page number into \u003ccode\u003eAL\u003c/code\u003e, and 16 for the\n\t\u003ccode\u003eOUT\u003c/code\u003e instruction itself. Therefore, the 8,000 page switches\n\trequired for EGC-copying a 320×200-pixel image require 136,000 cycles in\n\ttotal.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's the \u003ci\u003eoptimal\u003c/i\u003e case of using \u003ci\u003eonly\u003c/i\u003e those two\n\tinstructions. \u003ca href=\"/blog/2022-05-31\"\u003e📝 As I implied last time\u003c/a\u003e, TH01\n\tuses a \u003ci\u003efunction call\u003c/i\u003e for VRAM page switches, complete with creating\n\tand destroying a useless stack frame and unnecessarily updating a global\n\tvariable in main memory. I tried optimizing ZUN's code by throwing out\n\tunnecessary code and using \u003ca href=\"/blog/2022-02-18\"\u003e📝 pseudo-registers\u003c/a\u003e\n\tto generate probably optimal assembly code, and that did speed up the\n\tblitting to almost exactly 50% of the original version's run time. However,\n\tit did little about the flickering itself. Here's a comparison of the first\n\tloop with \u003ccode\u003eboom_duration = 16\u003c/code\u003e, recorded in DOSBox-X with\n\t\u003ccode\u003ecputype=auto\u003c/code\u003e and \u003ccode\u003ecycles=max\u003c/code\u003e, and with\n\t\u003ccode\u003ei\u003c/code\u003e overlaid using the text chip. Caution, flashing lights:\n\u003c/p\u003e\u003cfigure style=\"width: 320px\"\u003e\n\t\u003cfigcaption class=\"dynamic\"\u003e\u003cdiv\u003e\n\t\tThe original animation, completing in 50 frames instead of the expected\n\t\t34, thanks to slow blitting. Combined with the lack of\n\t\tdouble-buffering, this results in noticeable tearing as the screen\n\t\trefreshes while blitting is still in progress.\n\t\t(Note how the background of the \u003ci lang='ja' style='color: red'\n\t\u003eド﻿カ﻿ー﻿ン\u003c/i\u003e image is shifted 1 pixel to the left compared to pic\n\t#1.)\n\t\u003c/div\u003e\u003cdiv\u003e\n\t\tThis optimized version completes in the expected 34 frames. No tearing\n\t\thappens to be visible in this recording, but the \u003ci lang='ja'\n\t\tstyle='color: red'\u003eド﻿カ﻿ー﻿ン\u003c/i\u003e image is still visible on every\n\t\tsecond loop iteration. (Note how the background of the \u003ci lang='ja' style='color: red'\n\t\u003eド﻿カ﻿ー﻿ン\u003c/i\u003e image is shifted 1 pixel to the left compared to pic\n\t#1.)\n\t\u003c/div\u003e\u003c/figcaption\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.webp?b47668fb\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"320\" height=\"208\" data-fps=\"56.423132\" data-frame-count=\"51\" style=\"aspect-ratio: 320 / 208\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.avi?dc64f6dc\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.webm?4e6e6863\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.webm?03d55862\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.webm?c082fbc0\" type=\"video/webm\"\u003eVideo of the original, slow shake/boom effect during the TH01 Jigoku Bad Ending. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-original.avi?dc64f6dc\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.webp?b47668fb\" preload=\"none\" controls data-title=\"Optimized version\" loop width=\"320\" height=\"208\" data-fps=\"56.423132\" data-frame-count=\"35\" style=\"aspect-ratio: 320 / 208\" data-lossless=\"/blog/static/video/zmbv/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.avi?e37930dd\"\u003e\u003csource src=\"/blog/static/video/av1/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.webm?4a1bb5af\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.webm?74492239\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.webm?5f92d6f8\" type=\"video/webm\"\u003eVideo of an optimized shake/boom effect during the TH01 Jigoku Bad Ending. \u003ca href=\"/blog/static/video/zmbv/2022-06-17-TH01-Jigoku-Bad-Ending-shake-boom-optimized.avi?e37930dd\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tI pushed the optimized code to the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/th01_end_pic_optimize\"\u003e\u003ccode\u003eth01_end_pic_optimize\u003c/code\u003e\u003c/a\u003e\n\tbranch, to also serve as an example of how to get close to optimal code out\n\tof Turbo C++ 4.0J without writing a single ASM instruction.\u003cbr\u003e\n\tAnd if you really want to use the EGC for this, that's the best you can do.\n\tIt really sucks that it merely expanded the GRCG's 4×8-bit tile register to\n\t4×16 bits. With 32 bits, ≥386 CPUs could have taken advantage of their wider\n\tregisters and instructions to double the blitting performance. Instead, we\n\tnow know the reason why\n\t\u003ca href=\"/blog/2019-11-06\"\u003e📝 Promisence Soft's EGC-powered sprite driver that ZUN later stole for TH03\u003c/a\u003e\n\tis called SPRITE16 and not SPRITE32. What a massive disappointment.\n\u003c/p\u003e\u003cp\u003e\n\tBut what's perhaps a bigger surprise: \u003ci\u003eBlitting \u003cspan\n\ttitle=\"as seen in the on-disk layout of the .CDG and .CD2 files used in later games\"\u003eplanar\n\timages\u003c/span\u003e from main memory is much faster than EGC-powered inter-page\n\tVRAM copies\u003c/i\u003e, despite the required manual access to all 4 bitplanes. In\n\tfact, the blitting functions for the .CDG/.CD2 format, used from TH03\n\tonwards, would later demonstrate the optimal method of using \u003ccode\u003eREP\n\tMOVSD\u003c/code\u003e for blitting every line in 32-pixel chunks. If that was also\n\tused for these ending images, the core blitting operation would have taken\n\t\u003ccode\u003e((12\u0026nbsp;+\u0026nbsp;(3\u0026nbsp;×\u0026nbsp;(\u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"width in pixels\"\u003e320\u003c/span\u003e\u0026nbsp;/\u0026nbsp;\u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"1bpp pixels blitted per MOVSD instruction\"\n\t\u003e32\u003c/span\u003e)))\u0026nbsp;×\u0026nbsp;\u003cspan\n\t\tclass=\"hovertext\" title=\"height in pixels\"\u003e200\u003c/span\u003e\u0026nbsp;×\u0026nbsp;\u003cspan\n\t\tclass=\"hovertext\" title=\"number of bitplanes\"\u003e4\u003c/span\u003e) =\n\t33,600\u003c/code\u003e cycles, with not much more overhead for the surrounding row\n\tand bitplane loops. Sure, this doesn't factor in the whole infamous issue of\n\tVRAM being slow on PC-98, but the aforementioned 136,000 cycles don't even\n\tinclude any \u003ci\u003eactual blitting\u003c/i\u003e either. And as you move up to later PC-98\n\tmodels with Pentium CPUs, the gap between \u003ccode\u003eOUT\u003c/code\u003e and \u003ccode\u003eREP\n\tMOVSD\u003c/code\u003e only becomes larger. (Note that the page I linked above has a\n\ttypo in the cycle count of \u003ccode\u003eREP MOVSD\u003c/code\u003e on Pentium CPUs: According\n\tto the original Intel \u003ccite\u003eArchitecture and Programming Manual\u003c/cite\u003e, it's\n\t\u003ccode\u003e13﻿+﻿𝑛\u003c/code\u003e, not \u003ccode\u003e3﻿+﻿𝑛\u003c/code\u003e.)\u003cbr\u003e\n\tThis difference explains why later games rarely use EGC-\"accelerated\"\n\tinter-page VRAM copies, and keep all of their larger images in main memory.\n\tIt especially explains why TH04 and TH05 can get away with naively redrawing\n\tboss backdrop images on every frame.\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, the whole fact that ZUN did not define how long this image\n\tshould be visible is enough for me to increment the game's overall bug\n\tcounter. Who would have thought that looking at \u003ci\u003eendings\u003c/i\u003e of all things\n\twould teach us a PC-98 performance lesson… Sure, optimizing TH01 already\n\tseemed promising just by looking at its bloated code, but I had no idea that\n\tits performance issues extended so far past that level.\n\u003c/p\u003e\u003cp\u003e\n\tThat only leaves the common beginning part of all endings and a short\n\t\u003ccode\u003emain()\u003c/code\u003e function before we're done with \u003ccode\u003eFUUIN.EXE\u003c/code\u003e,\n\tand 98 functions until all of TH01 is decompiled! Next up: SinGyoku, who not\n\tonly is the quickest boss to defeat in-game, but also comes with the least\n\tamount of code. See you very soon!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-06-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-05-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-06-17T17:03:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-05-31",
      "url": "https://rec98.nmlgc.net/blog/2022-05-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-06-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-04-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-05-31\"\u003e\u003ctime datetime=\"2022-05-31T23:43:30Z\"\u003e2022-05-31 23:43\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0193\"\u003eP0193\u003c/a\u003e\n\t\t\tTH01 decompilation (Elis, part 1/4: Preparations + patterns 1-3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e1f3f9f...183d7a2\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0194\"\u003eP0194\u003c/a\u003e\n\t\t\tTH01 decompilation (Elis, part 2/4: Patterns 4-6 + transformations)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/183d7a2...5d93a50\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0195\"\u003eP0195\u003c/a\u003e\n\t\t\tTH01 decompilation (Elis, part 3/4: Patterns 7-13)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5d93a50...e18c53d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0196\"\u003eP0196\u003c/a\u003e\n\t\t\tTH01 decompilation (Elis, part 4/4: Entrance animation + main function)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e18c53d...57c9ac5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0197\"\u003eP0197\u003c/a\u003e\n\t\t\tTH01 research (HP bar heap corruption + boss defeat crashes) + decompilation (Verdict screen)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/57c9ac5...48db0b7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/elis\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 魔界/Makai route.\"\u003eelis\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWith Elis, we've not only reached the midway point in TH01's boss code, but\n\talso a bunch of other milestones: Both \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e and TH01 as\n\ta whole have crossed the 75% RE mark, and overall position independence has\n\talso finally cracked 80%!\n\u003c/p\u003e\u003cp\u003e\n\tAnd it got done in 4 pushes again? Yup, we're back to\n\t\u003ca href=\"/blog/2021-08-23\"\u003e📝 Konngara\u003c/a\u003e levels of redundancy and\n\tcopy-pasta. This time, it didn't even stop at the big copy-pasted code\n\tblocks for the rift sprite and 256-pixel circle animations, with the words\n\t\"redundant\" and \"unnecessary\" ending up a total of 18 times in my source\n\tcode comments.\u003cbr\u003e\n\tBut damn is this fight broken. As usual with TH01 bosses, let's start with a\n\thigh-level overview:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe Elis fight consists of 5 phases (excluding the entrance animation),\n\twhich must be completed in order.\u003c/li\u003e\n\t\u003cli\u003eIn all odd-numbered phases, Elis uses a random one-shot danmaku pattern\n\tfrom an exclusive per-phase pool before teleporting to a random\n\tposition.\u003cul\u003e\n\t\t\u003cli\u003eThere are 3 exclusive girl-form patterns per phase, plus 4\n\t\tadditional bat-form patterns in phase 5, for a total of 13.\u003c/li\u003e\n\t\t\u003cli\u003eDue to a quirk in the selection algorithm in phases 1 and 3, there\n\t\tis a 25% chance of Elis skipping an attack cycle and just teleporting\n\t\tagain.\u003c/li\u003e\n\t\t\u003cli\u003eIn contrast to Konngara, Elis can freely select the same pattern\n\t\tmultiple times in a row. There's nothing in the code to prevent that\n\t\tfrom happening.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eThis pattern+teleport cycle is repeated until Elis' HP reach a certain\n\tthreshold value. The odd-numbered phases correspond to the white (phase 1),\n\tred-white (phase 3), and red (phase 5) sections of the health bar. However,\n\tthe next phase can only start at the end of each cycle, after a\n\tteleport.\u003c/li\u003e\n\t\u003cli\u003ePhase 2 simply teleports Elis back to her starting screen position of\n\t(﻿320,\u0026nbsp;144﻿) and then advances to phase 3.\u003c/li\u003e\n\t\u003cli\u003ePhase 4 does the same as phase 2, but adds the initial bat form\n\ttransformation before advancing to phase 5.\u003c/li\u003e\n\t\u003cli\u003ePhase 5 replaces the teleport with a transformation to the bat form.\n\tRather than teleporting instantly to the target position, the bat gradually\n\tflies there, firing a randomly selected looping pattern from the 4-pattern\n\tbat pool on the way, before transforming back to the girl form.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThis puts the earliest possible end of the fight at the first frame of phase\n\t5. However, nothing prevents Elis' HP from reaching 0 before that point. You\n\tcan nicely see this in \u003ca href=\"/blog/2020-05-12\"\u003e📝 debug mode\u003c/a\u003e: Wait\n\tuntil the HP bar has filled up to avoid heap corruption, hold ↵\u0026nbsp;Return\n\tto reduce her HP to 0, and watch how Elis still goes through a total of\n\t\u003cspan class=\"hovertext\" title=\"With each of them having the aforementioned\n\t25% chance of being replaced with a teleport.\"\u003etwo patterns*\u003c/span\u003e and four\n\tteleport animations before accepting defeat.\n\u003c/p\u003e\u003cp\u003e\n\tBut wait, heap corruption? Yup, there's a bug in the HP bar that already\n\taffected Konngara as well, and it isn't even \u003ci\u003ejust\u003c/i\u003e about the graphical\n\tglitches generated by negative HP:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eBecause \u003ca href=\"/blog/2020-12-18\"\u003e📝 the backgrounds behind the points\tare kept in main memory\u003c/a\u003e,\n\tthere has to be a cap on the number of hitpoints, which turns out to be\n\t96.\u003c/li\u003e\n\t\u003cli\u003eThe initial fill-up animation is drawn to both VRAM pages at a rate of 1\n\tHP per frame… by passing the current frame number as the\n\t\u003ccode\u003ecurrent_hp\u003c/code\u003e number. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eThe \u003ccode\u003etarget_hp\u003c/code\u003e is indicated by simply passing the current\n\tHP…\u003c/li\u003e\n\t\u003cli\u003e… which, however, can be reduced in debug mode at an equal rate of up to\n\t1 HP per frame.\u003c/li\u003e\n\t\u003cli\u003eThe completion condition only checks if\n\t\u003ccode\u003e((target_hp\u0026nbsp;-\u0026nbsp;1)\u0026nbsp;==\u0026nbsp;current_hp)\u003c/code\u003e. With the\n\tright timing, both numbers can therefore run past each other.\u003c/li\u003e\n\t\u003cli\u003eIn that case, the function is repeatedly called on every frame, backing\n\tup the original VRAM contents for the current HP point before blitting\n\tit…\u003c/li\u003e\n\t\u003cli\u003e… until frame \u003ccode\u003e((96\u0026nbsp;/\u0026nbsp;2)\u0026nbsp;+\u0026nbsp;1)\u003c/code\u003e, where the\n\t.PTN slot pointer overflows the heap buffer and overwrites whatever comes\n\tafter. \u003ca href=\"/blog/2020-11-30\"\u003e📝 Sounds familiar, right?\u003c/a\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSince Elis starts with 14 HP, which is an even number, this corruption is\n\ttrivial to cause: Simply hold ↵\u0026nbsp;Return from the beginning of the\n\tfight, and the completion condition will never be \u003ccode\u003etrue\u003c/code\u003e, as the\n\tHP and frame numbers run past the off-by-one meeting point.\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-heap-corruption.webp?8f2697a4\" preload=\"none\" controls width=\"640\" height=\"224\" data-fps=\"56.423132\" data-frame-count=\"168\" style=\"aspect-ratio: 640 / 224\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-heap-corruption.avi?82a64f9c\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-heap-corruption.webm?99e37211\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-heap-corruption.webm?4bd84e11\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-heap-corruption.webm?c7df17fb\" type=\"video/webm\"\u003eVideo demonstrating TH01 Elis' HP going below zero, and how rendering negative HP will cause heap corruption. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-heap-corruption.avi?82a64f9c\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\t\u003cstrong\u003eEdit (2023-07-21):\u003c/strong\u003e Pressing ↵\u0026nbsp;Return to reduce HP\n\t\talso works in test mode (\u003ccode\u003egame t\u003c/code\u003e). There, the game doesn't\n\t\teven check the heap, and consequently won't report any corruption,\n\t\tallowing the HP bar to be glitched even further.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tRegular gameplay, however, entirely prevents this due to the fixed start\n\tpositions of Reimu and the Orb, the Orb's fixed initial trajectory, and the\n\t50 frames of delay until a bomb deals damage to a boss. These aspects make\n\tit impossible to hit Elis within the first 14 frames of phase 1, and ensure\n\tthat her HP bar is always filled up completely. So ultimately, this bug ends\n\tup comparable in seriousness to the\n\t\u003ca href=\"/blog/2020-05-12\"\u003e📝 recursion\u0026nbsp;/ stack overflow bug in the memory info screen\u003c/a\u003e.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThese wavy teleport animations point to a quite frustrating architectural\n\tissue in this fight. It's not even the fact that unblitting the yellow star\n\tsprites rips temporary holes into Elis' sprite; that's almost expected from\n\tTH01 at this point. Instead, it's all because of this unused frame of the\n\tanimation:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\u003cimg\nsrc=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABgBAMAAAAnVGd6AAAAIVBMVEUAAADw8PDw0LDQoJCwoABgYGDQAACAEBAwMDCgAABwAFDmaxhHAAAAAXRSTlMAQObYZgAAAhpJREFUeAHtwUF24zgQBcEPAqzJyvsfeB4oyfZ095KahdsR+TMueQHI3wlAlUsSLj5A3gZwgwTIRpLgBcjtADdIQjckAZIEHyDvAF5IEqq7gK4iJPhE3gfwQrYqtsWWBFEFyNsBqkAurLUWQP1DJUG8APkL4BMhCQtYTBZAkoAPuRm+kEzCMRbMyXaQBPxAboIvJJmsNWHOybHgYPMTuZkXknCsCRxzcsBiHhP8BHkLVFGShAVyHHgUExIALwh5IwA3FBLmoo6DOY/jYC1ARcUN8p3hf8AEJsdRx3GwFguOAx/y4wZe1mROxqCLCc05xoA5WfiJvIGKyFprMU6qm+4qxhgs1gJVQBXyFoCAisBiMQZUFWdXcZ6DxVoLVEAVcj/8AhYLBnSdg0E15wDWWgtfcjt8QV24ZEAzBoNtKGstcMuPG/jpBDhPTp4EtzHEC7mf4guX7obm5EF1jIEXyLeGH/AJoLeqbs5T0KEOH3I3/B3Q3VVdVd1sw5d8A+AT+ELXh27cUAIoQSA3EV+or7q9kCRIkii5HYoCKvULVEkSyAXI94ZkA5IE7fpVq7mQ98A8QDao3yHZyL0gD+RC/QHkbpgHcqG+6rpANvIEuQ/ZqM2ytu6q0iIbeYH8eD/yRBJKyyqtS1tbk4S8EXkhoT5YCpZVBUnI/cgXXU9WVXV3VxWahNyMPBEeuru23kDJF5DbkShbPwCa/8G/V+gpftGVucEAAAAASUVORK5CYII=\"\nalt=\"An unused wave animation frame from TH01's BOSS5.BOS\" style=\"height: 96px;\"\n\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith this sprite still being part of \u003ccode\u003eBOSS5.BOS\u003c/code\u003e, Girl-Elis has a\n\ttotal of 9 animation frames, 1 more than the\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 8 per-entity sprites allowed by ZUN's architecture\u003c/a\u003e.\n\tThe quick and easy solution would have been to simply bump the sprite array\n\tsize by 1, but… nah, this would have added another 20 bytes to all 6 of the\n\t.BOS image slots. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Instead, ZUN wrote the manual\n\tposition synchronization code I mentioned in that 2020 blog post.\n\tIronically, he then copy-pasted this snippet of code often enough that it\n\tended up taking up more than 120 bytes in the Elis fight alone – with, you\n\tguessed it, some of those copies being redundant. Not to mention that just\n\tgoing from 8 to 9 sprites would have allowed ZUN to go down from 6 .BOS\n\timage slots to 3. That would have actually \u003ci\u003esaved\u003c/i\u003e 420 bytes in\n\taddition to the manual synchronization trouble. Looking forward to SinGyoku,\n\tthat's going to be fun again…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs for the fight itself, it doesn't take long until we reach its most janky\n\tdanmaku pattern, right in phase 1:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-3-original.webp?7395b327\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"245\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-3-original.avi?50d657ed\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-3-original.webm?3b52d83e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-3-original.webm?a32a7769\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-3-original.webm?04500118\" type=\"video/webm\"\u003eVideo of TH01 Elis' \"pellets along circle\" pattern in its original version. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-3-original.avi?50d657ed\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-3-fixed.webp?7395b327\" preload=\"none\" controls data-title=\"Dequirked version\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"245\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-3-fixed.avi?dd2ff080\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-3-fixed.webm?48a26934\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-3-fixed.webm?e3956e5d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-3-fixed.webm?5d471b9a\" type=\"video/webm\"\u003eVideo of TH01 Elis' \"pellets along circle\" pattern, with the pellet camp bumped to 160, fixed blitting for the 4th quadrant of the big circle, and a correctly centered pellet spawn point. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-3-fixed.avi?dd2ff080\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe \"pellets along circle\" pattern on Lunatic, in its original version\n\t\tand with fanfiction fixes for everything that can potentially be\n\t\tinterpreted as a bug.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cul\u003e\n\t\u003cli\u003eFor whatever reason, the lower-right quarter of the circle isn't\n\tanimated? This animation works by only drawing the new dots added with every\n\tsubsequent animation frame, expressed as a tiny arc of a dotted circle. This\n\tarc starts at the animation's current 8-bit angle and ends on the sum of\n\tthat angle and a hardcoded constant. In every other (copy-pasted, and\n\tcorrect) instance of this animation, ZUN uses \u003ccode\u003e0x02\u003c/code\u003e as the\n\tconstant, but this one uses… \u003ccode\u003e0.05\u003c/code\u003e for the lower-right quarter?\n\tAs in, a 64-bit \u003ccode\u003edouble\u003c/code\u003e constant that truncates to 0 when added\n\tto an 8-bit integer, thus leading to the start and end angles being\n\tidentical and the game not drawing anything.\u003c/li\u003e\n\t\u003cli\u003eOn Easy and Normal, the pattern then spawns 32 bullets along the outline\n\tof the circle, no problem there. On Lunatic though, every one of these\n\tbullets is instead turned into a narrow-angled 5-spread, resulting in 160\n\tpellets… in a game with a pellet cap of 100. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\tNow, if Elis teleported herself to a position near the top of the playfield,\n\tmost of the capped pellets would have been clipped at that top edge anyway,\n\tsince the bullets are spawned in clockwise order starting at Elis' right\n\tside with an angle of \u003ccode\u003e0x00\u003c/code\u003e. On lower positions though, you can\n\tdefinitely see a difference if the cap were high enough to allow all coded\n\tpellets to actually be spawned.\u003cbr\u003e\n\tThe Hard version gets dangerously close to the cap by spawning a total of 96\n\tpellets. Since this is the only pattern in phase 1 that fires pellets\n\tthough, you are guaranteed to see all of the unclipped ones.\u003c/li\u003e\n\t\u003cli\u003eThe pellets also aren't spawned exactly on the telegraphed circle, but 4 pixels to the left.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThen again, it might very well be that all of this was intended, or, most\n\tlikely, just left in the game as a happy accident. The latter interpretation\n\twould explain why ZUN didn't just delete the rendering calls for the\n\tlower-right quarter of the circle, because seriously, how would you not spot\n\tthat? The phase 3 patterns continue with more minor graphical glitches that\n\taren't even worth talking about anymore.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd then Elis transforms into her bat form at the beginning of Phase 5,\n\twhich displays some rather unique hitboxes. The one against the Orb is fine,\n\tbut the one against player shots…\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-bat-shot-hitbox.webp?7c45938a\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"582\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-bat-shot-hitbox.avi?0a935997\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-bat-shot-hitbox.webm?70ed808d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-bat-shot-hitbox.webm?a6d011e5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-bat-shot-hitbox.webm?2ca902ee\" type=\"video/webm\"\u003eVideo demonstrating the hitboxes of TH01 Elis' bat form. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-bat-shot-hitbox.avi?0a935997\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\t… uses the bat's X coordinate for both X and Y dimensions.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e In regular gameplay, it's not \u003ci\u003etoo\u003c/i\u003e bad as most\n\tof the bat patterns fire aimed pellets which typically don't allow you to\n\tmove below her sprite to begin with. But if you ever tried destroying these\n\tpellets while standing near the middle of the playfield, now you know why\n\tthat didn't work. This video also nicely points out how the bat, like any\n\tboss sprite, is only ever blitted at positions on the 8×1-pixel VRAM byte\n\tgrid, while collision detection uses the actual pixel position.\n\u003c/p\u003e\u003cp\u003e\n\tThe bat form patterns are all relatively simple, with little variation\n\tdepending on the difficulty level, except for the \"slow pellet spreads\"\n\tpattern. This one is almost easiest to dodge on Lunatic, where the 5-spreads\n\tare not only always fired downwards, but also at the hardcoded narrow delta\n\tangle, leaving plenty of room for the player to move out of the way:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-7-Easy.webp?843f603e\" preload=\"none\" controls data-title=\"Easy\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"109\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Easy.avi?879ab29f\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-7-Easy.webm?66453069\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-7-Easy.webm?92a9068e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-7-Easy.webm?34d1164d\" type=\"video/webm\"\u003eVideo of the \"slow spread\" pattern of TH01 Elis' bat form on Easy Mode. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Easy.avi?879ab29f\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-7-Normal.webp?1b1981dc\" preload=\"none\" controls data-title=\"Normal\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"109\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Normal.avi?3f534055\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-7-Normal.webm?e3256fb9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-7-Normal.webm?8ce93cf3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-7-Normal.webm?f039792c\" type=\"video/webm\"\u003eVideo of the \"slow spread\" pattern of TH01 Elis' bat form on Normal Mode. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Normal.avi?3f534055\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-7-Hard.webp?d0e4ead9\" preload=\"none\" controls data-title=\"Hard\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"109\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Hard.avi?927b49a8\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-7-Hard.webm?5ef3b830\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-7-Hard.webm?65393e6b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-7-Hard.webm?f0de9eae\" type=\"video/webm\"\u003eVideo of the \"slow spread\" pattern of TH01 Elis' bat form on Hard Mode. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Hard.avi?927b49a8\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-7-Lunatic.webp?1b4aa8c3\" preload=\"none\" controls data-title=\"Lunatic\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"109\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Lunatic.avi?72c48968\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-7-Lunatic.webm?fe2e51cf\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-7-Lunatic.webm?eaa4e6e2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-7-Lunatic.webm?366a1256\" type=\"video/webm\"\u003eVideo of the \"slow spread\" pattern of TH01 Elis' bat form on Lunatic Mode. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-7-Lunatic.avi?72c48968\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003eThe \"slow pellet spreads\" pattern of Elis' bat form, on every\n\tdifficulty. Which version do you think is the easiest one?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFinally, we've got another potential timesave in the girl form's \"safety\n\tcircle\" pattern:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-05-31-TH01-Elis-pattern-12.webp?efcaf89b\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"397\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-12.avi?1d674abc\"\u003e\u003csource src=\"/blog/static/video/av1/2022-05-31-TH01-Elis-pattern-12.webm?c233ced7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-05-31-TH01-Elis-pattern-12.webm?2f6365fd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-05-31-TH01-Elis-pattern-12.webm?c1339252\" type=\"video/webm\"\u003eVideo of TH01 Elis' \"safety circle + rain from top\" pattern. \u003ca href=\"/blog/static/video/zmbv/2022-05-31-TH01-Elis-pattern-12.avi?1d674abc\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"117\" data-title=\"Circle activates\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003crec98-video-marker data-frame=\"317\" data-title=\"Circle disappears\" data-alignment=\"left\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAfter the circle spawned completely, you lose a life by moving outside it,\n\tbut doing that immediately advances the pattern past the circle part. This\n\tpart takes 200 frames, but the defeat animation only takes 82 frames, so\n\tyou can save up to 118 frames there.\n\u003c/p\u003e\u003cp\u003e\n\tFinal funny tidbit: As with all dynamic entities, this circle is only\n\tblitted to VRAM page 0 to allow easy unblitting. However, it's also kind of\n\tstatic, and there needs to be some way to keep the Orb, the player shots,\n\tand the pellets from ripping holes into it. So, ZUN just re-blits the circle\n\tevery… 4 frames?! 🤪 The same is true for the Star of David and its\n\tsurrounding circle, but there you at least get a flash animation to justify\n\tit. All the overlap is actually quite a good reason for not even attempting\n\tto \u003ca href=\"/blog/2021-11-08\"\u003e📝 mess with the hardware color palette instead\u003c/a\u003e.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's the 4th PC-98 Touhou boss decompiled, 27 to go… but wait, all\n\tthese quirks, and I still got nothing about \u003ca\n\thref=\"https://youtu.be/Al0KTB_0u4A?t=80\"\u003ethe one actual crash that can\n\tappear in regular gameplay\u003c/a\u003e? \u003ca\n\thref=\"https://www.youtube.com/watch?v=3YjHkrJ1BBM\"\u003eThere has even been a\n\trecent video about it.\u003c/a\u003e The cause \u003ci\u003ehas\u003c/i\u003e to be in Elis' main\n\tfunction, after entering the defeat branch and before the blocking white-out\n\tanimation. It \u003ci\u003ecan't\u003c/i\u003e be anywhere else other than in the\n\t\u003ca href=\"/blog/2020-01-14\"\u003e📝 central line blitting and unblitting \tfunction\u003c/a\u003e,\n\tcalled from \u003ca href=\"/blog/2020-10-06\"\u003e📝 that one broken laser reset+unblit function\u003c/a\u003e,\n\tbecause everything else in that branch looks fine… and I think we can rule\n\tout a crash in MDRV2's non-blocking fade-out call. That's going to need some\n\textra research, and a 5th push added on top of this delivery.\n\u003c/p\u003e\u003cp\u003e\n\tReproducing the crash was the whole challenge here. Even after moving Elis\n\tand Reimu to the exact positions seen in Pearl's video and setting Elis' HP\n\tto 0 on the exact same frame, everything ran fine for me. It's definitely no\n\tdivision by 0 this time, the function perfectly guards against that\n\tpossibility. The line specified in the function's parameters is always\n\tclipped to the VRAM region as well, so we can also rule out illegal memory\n\taccesses here…\n\u003c/p\u003e\u003cp\u003e\n\t… or can we? Stepping through it all reminded me of how this function brings\n\tunblitting sloppiness to the next level: For each VRAM byte touched, ZUN\n\tactually unblits \u003ci\u003ethe 4 surrounding bytes\u003c/i\u003e, adding one byte to the left\n\tand two bytes to the right, and using a single 32-bit read and write per\n\tbitplane. So what happens if the function tries to unblit the topmost byte\n\tof VRAM, covering the pixel positions from (﻿0,\u0026nbsp;0﻿) to (﻿7,\u0026nbsp;0﻿)\n\tinclusive? The VRAM offset of \u003ccode\u003e0x0000\u003c/code\u003e is decremented to\n\t\u003ccode\u003e0xFFFF\u003c/code\u003e to cover the one byte to the left, 4 bytes are written\n\tto this address, the CPU's internal offset overflows… and as it turns out,\n\tthat is illegal even in Real Mode as of the 80286, and \u003cspan\n\tclass=\"hovertext\" title=\"Bug counter: 1\"\u003ewill raise a General Protection\n\tFault\u003c/span\u003e. Which is… ignored by \u003ca\n\thref=\"https://github.com/joncampbell123/dosbox-x/issues/3526\"\u003eDOSBox-X\u003c/a\u003e,\n\tevery Neko Project II version in common use, the \u003ca\n\thref=\"http://takeda-toshiya.my.coocan.jp/common/index.html\"\u003eCSCP\n\temulators\u003c/a\u003e, SL9821, and T98-Next. Only Anex86 accurately emulates the\n\tbehavior of real hardware here.\n\u003c/p\u003e\u003cp\u003e\n\tOK, but no laser fired by Elis ever reaches the top-left corner of the\n\tscreen. How can such a fault even happen in practice? That's where the\n\tbroken laser reset+unblit function comes in: Not only does it just \u003cspan\n\tclass=\"hovertext\" title=\"Bug counter: 2\"\u003eflat out pass the wrong\n\tparameters\u003c/span\u003e to the line unblitting function – describing the line\n\t\u003ci\u003ealready traveled\u003c/i\u003e by the laser and stopping where the laser begins –\n\tbut it also \u003cspan class=\"hovertext\" title=\"Bug counter: 3\"\u003epasses them\n\twrongly, in the form of raw 32-bit fixed-point Q24.8 values\u003c/span\u003e, with no\n\tconversion other than a truncation to the signed 16-bit pixels expected by\n\tthe function. What then follows is an attempt at interpolation and clipping\n\tto find a line segment between those garbage coordinates that actually falls\n\twithin the boundaries of VRAM:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003e\u003ccode\u003eright/bottom\u003c/code\u003e correspond to a laser's origin position, and\n\t\u003ccode\u003eleft/top\u003c/code\u003e to the leftmost pixel of its moved-out top line. The\n\tbug therefore only occurs with lasers that stopped growing and have started\n\tmoving.\u003c/li\u003e\n\t\u003cli\u003eMoreover, it will only happen if either \u003ccode\u003e(left % 256)\u003c/code\u003e or\n\t\u003ccode\u003e(right % 256)\u003c/code\u003e is ≤ 127 and the other one of the two is ≥ 128.\n\tThe typecast to signed 16-bit integers then turns the former into a large\n\tpositive value and the latter into a large negative value, triggering the\n\tfunction's clipping code.\u003c/li\u003e\n\t\u003cli\u003eThe function then follows \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm\"\u003eBresenham's\n\talgorithm\u003c/a\u003e: \u003ccode\u003eleft\u003c/code\u003e is ensured to be smaller than \u003ccode\u003eright\u003c/code\u003e\n\tby swapping the two values if necessary. If that happened, \u003ccode\u003etop\u003c/code\u003e\n\tand \u003ccode\u003ebottom\u003c/code\u003e are also swapped, regardless of their value – the\n\talgorithm does not care about their order.\u003c/li\u003e\n\t\u003cli\u003eThe slope in the X dimension is calculated using an integer division of\n\t\u003ccode\u003e(﻿(﻿bottom\u0026nbsp;-\u0026nbsp;top﻿)\u0026nbsp;/\n\t(﻿right\u0026nbsp;-\u0026nbsp;left﻿)﻿)\u003c/code\u003e. Both subtractions are done on signed\n\t16-bit integers, and overflow accordingly.\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003e(-left\u0026nbsp;×\u0026nbsp;slope_x)\u003c/code\u003e is added to \u003ccode\u003etop\u003c/code\u003e,\n\tand \u003ccode\u003eleft\u003c/code\u003e is set to 0.\u003c/li\u003e\n\t\u003cli\u003eIf both \u003ccode\u003etop\u003c/code\u003e and \u003ccode\u003ebottom\u003c/code\u003e are \u0026lt; 0 or\n\t≥\u0026nbsp;640, there's nothing to be unblitted. Otherwise, the final\n\tcoordinates are clipped to the VRAM range of [﻿(﻿0,\u0026nbsp;0﻿),\n\t(﻿639,\u0026nbsp;399﻿)﻿].\u003c/li\u003e\n\t\u003cli\u003eIf the function got this far, the line to be unblitted is now very\n\tlikely to reach from\u003col\u003e\n\t\t\u003cli\u003ethe top-left to the bottom-right corner, starting out at\n\t\t(﻿0,\u0026nbsp;0﻿) right away, or\u003c/li\u003e\n\t\t\u003cli\u003efrom the bottom-left corner to the top-right corner. In this case,\n\t\tyou'd expect unblitting to end at (﻿639,\u0026nbsp;0﻿), but thanks to an\n\t\t\u003cspan class=\"hovertext\" title=\"Bug counter: 4\"\u003eoff-by-one error\u003c/span\u003e,\n\t\tit actually ends at (﻿640,\u0026nbsp;-1﻿), which is equivalent to\n\t\t(﻿0,\u0026nbsp;0﻿). Why add clipping to VRAM offset calculations when\n\t\teverything else is clipped already, right? \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003c/ol\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cimg src=\"/blog/static/2022-05-31-TH01-Elis-GPF-debug.png?344afed2\" class=\"th01_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-05-31-TH01-Mima-GPF-debug.png?3fd5257a\" class=\"th01_playfield\"\u003e\n\t\u003cfigcaption\u003ePossible laser states that will cause the fault, with some debug\n\toutput to help understand the cause, and any pellets removed for better\n\treadability. This can happen for all bosses that can potentially have\n\tshootout lasers on screen when being defeated, so it also applies to Mima.\n\tFixing this is easier than understanding why it happens, but since y'all\n\tlove reading this stuff…\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\ttl;dr: TH01 has a high chance of freezing at a boss defeat sequence if there\n\tare diagonally moving lasers on screen, \u003ci\u003eand\u003c/i\u003e if your PC-98 system\n\traises a General Protection Fault on a 4-byte write to offset\n\t\u003ccode\u003e0xFFFF\u003c/code\u003e, \u003ci\u003eand\u003c/i\u003e if you don't run a TSR with an \u003ccode\u003eINT\n\t0Dh\u003c/code\u003e handler that might handle this fault differently.\n\u003c/p\u003e\u003cp\u003e\n\tThe easiest fix option would be to just remove the attempted laser\n\tunblitting entirely, but that would also have an impact on this game's…\n\t\u003ci\u003edistinctive\u003c/i\u003e visual glitches, in addition to touching a whole lot of\n\tcode bytes. If I ever get funded to work on a hypothetical TH01 Anniversary\n\tEdition that completely rearchitects the game to fix all these glitches, it\n\twould be appropriate there, but not for something that purports to be the\n\toriginal game.\n\u003c/p\u003e\u003cp\u003e\n\t(Sidenote to further hype up this Anniversary Edition idea for PC-98\n\thardware owners: With the amount of performance left on the table at every\n\tcorner of this game, I'm pretty confident that we can get it to work\n\tdecently on PC-98 models with just an 80286 CPU.)\n\u003c/p\u003e\u003cp\u003e\n\tSince we're in critical infrastructure territory once again, I went for the\n\tmost conservative fix with the least impact on the binary: Simply changing\n\tany VRAM offsets \u003ccode\u003e\u0026gt;= 0xFFFD\u003c/code\u003e to \u003ccode\u003e0x0000\u003c/code\u003e to avoid\n\tthe GPF, and leaving all other bugs in place. Sure, it's rather lazy and\n\t\"incorrect\"; the function still unblits a 32-pixel block there, but adding a\n\tspecial case for blitting 24 pixels would add way too much code. And\n\tseriously, it's not like anything happens in the 8 pixels between\n\t(﻿24,\u0026nbsp;0﻿) and (﻿31,\u0026nbsp;0﻿) inclusive during gameplay to begin with.\n\tTo balance out the additional per-row \u003ccode\u003eif()\u003c/code\u003e branch, I inlined\n\tthe VRAM page change I/O, saving two function calls and one memory write per\n\tunblitted row.\n\u003c/p\u003e\u003cp\u003e\n\tThat means it's time for a new \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/community_choice_fixes\"\u003e\u003ccode\u003ecommunity_choice_fixes\u003c/code\u003e\u003c/a\u003e\n\tbuild, containing the new definitive bugfixed versions of these games:\n\t\u003ca class=\"download\" href=\"/blog/static/2022-05-31-community-choice-fixes.zip?2f61a66c\" data-kb=\"227.7\"\u003e2022-05-31-community-choice-fixes.zip \u003c/a\u003e\n\tCheck the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/th01_critical_fixes\"\u003e\u003ccode\u003eth01_critical_fixes\u003c/code\u003e\u003c/a\u003e\n\tbranch for the modified TH01 code. It also contains a fix for the HP bar\n\theap corruption in test or debug mode – simply changing the \u003ccode\u003e==\u003c/code\u003e\n\tcomparison to \u003ccode\u003e\u0026lt;=\u003c/code\u003e is enough to avoid it, and negative HP will\n\tstill create aesthetic glitch art.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOnce again, I then was left with ½ of a push, which I finally filled with\n\tsome \u003ccode\u003eFUUIN.EXE\u003c/code\u003e code, specifically the verdict screen. The most\n\tinteresting part here is the player title calculation, which is quite\n\tsneaky: There are only 6 skill \u003ci\u003elevels\u003c/i\u003e, but three \u003ci\u003egroups\u003c/i\u003e of\n\ttitles for each level, and the title you'll see is picked from a random\n\tgroup. It looks like this is the first time anyone has documented the\n\tcalculation?\u003cbr\u003e\n\tAs for the levels, ZUN definitely didn't expect players to do particularly\n\twell. With a 1cc being the standard goal for completing a Touhou game, it's\n\tespecially funny how TH01 expects you to continue \u003ci\u003ea lot\u003c/i\u003e: The code has\n\tbranches for up to 21 continues, and the on-screen table explicitly leaves\n\troom for 3 digits worth of continues \u003ci\u003eper 5-stage scene\u003c/i\u003e. Heck, these\n\tcounts are even stored in 32-bit \u003ccode\u003elong\u003c/code\u003e variables.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: \u003ca href=\"/blog/2021-07-20\"\u003e📝 Finally finishing\u003c/a\u003e the long\n\toverdue \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e MediaWiki update work, while continuing with\n\tKikuri in the meantime. Originally I wasn't sure about what to do between\n\tElis and \u003ca href=\"https://twitter.com/ReC98Project/status/1521279556174417920\"\u003eSeihou\u003c/a\u003e,\n\tbut with Ember2528's surprise\n\t\u003cscript\u003eformatCurrency(2000)\u003c/script\u003e contribution last week, y'all have\n\tdemonstrated more than enough interest in the idea of getting TH01 done\n\tsooner rather than later. And I agree – after all, we've got the 25th\n\tanniversary of its first public release coming up on August 15, and I might\n\tstill manage to completely decompile this game by that point…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-06-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-04-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-05-31T23:43:30Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-04-30",
      "url": "https://rec98.nmlgc.net/blog/2022-04-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-05-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-04-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-04-30\"\u003e\u003ctime datetime=\"2022-04-30T22:58:07Z\"\u003e2022-04-30 22:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0190\"\u003eP0190\u003c/a\u003e\n\t\t\tTH05 decompilation (Shinki, part 1/2: Patterns 1-10)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5734815...293e16a\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0191\"\u003eP0191\u003c/a\u003e\n\t\t\tTH05 decompilation (Shinki, part 2/2: Pattern 11, main function)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/293e16a...71cb7b5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0192\"\u003eP0192\u003c/a\u003e\n\t\t\tTH05 decompilation (Stage 1 midboss + Replay file format) + Separating translation units, part 11\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/71cb7b5...e1f3f9f\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003enrook, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThe important things first:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eTH05 has passed the 50% RE mark, with both \u003ccode\u003eMAIN.EXE\u003c/code\u003e and the\n\tgame as a whole! With that, we've also reached what \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\n\twanted out of the project, so he's suspending his discount offer for a\n\tbit.\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003eCurve bullets\u003c/i\u003e are now officially called \u003ci\u003echeetos\u003c/i\u003e! \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1500256959785746434\"\u003e76.7% of\n\tfans prefer this term\u003c/a\u003e, and it fits into the 8.3 DOS filename scheme much\n\tbetter than \u003ci\u003ehoming lasers\u003c/i\u003e (as they're called in\n\t\u003ccode\u003eOMAKE.TXT\u003c/code\u003e) or \u003ci\u003e\u003ca\n\thref=\"https://twitter.com/armormodechang/status/1500907580914343943\"\u003eTaito\n\tlasers\u003c/a\u003e\u003c/i\u003e (which would indeed have made sense as well).\u003c/li\u003e\n\t\u003cli\u003e…oh, and I managed to decompile Shinki within 2 pushes after all. That\n\tleft enough budget to also add the Stage 1 midboss on top.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSo, Shinki! As far as final boss code is concerned, she's surprisingly\n\teconomical, with \u003ca href=\"/blog/2021-06-10\"\u003e📝 her background animations\u003c/a\u003e\n\tmaking up more than ⅓ of her entire code. Going straight from TH01's\n\t\u003ca href=\"/blog/2021-08-23\"\u003e📝 final\u003c/a\u003e\n\t\u003ca href=\"/blog/2022-01-31\"\u003e📝 bosses\u003c/a\u003e\n\tto TH05's final boss definitely showed how much ZUN had streamlined\n\tdanmaku pattern code by the end of PC-98 Touhou. Don't get me wrong, there\n\tis still room for improvement: TH05 not only\n\t\u003ca href=\"/blog/2022-03-27\"\u003e📝 reuses the same 16 bytes of generic boss state we saw in TH04 last month\u003c/a\u003e,\n\tbut also uses them 4× as often, and even for midbosses. Most importantly\n\tthough, defining danmaku patterns using a single global instance of the\n\tgroup template structure is just bad no matter how you look at it:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe script code ends up rather bloated, with a single \u003ccode\u003eMOV\u003c/code\u003e\n\tinstruction for setting one of the fields taking up 5 bytes. By comparison,\n\tthe entire structure for regular bullets is 14 bytes large, while the\n\ttemplate structure for Shinki's 32×32 ball bullets could have easily been\n\treduced to 8 bytes.\u003c/li\u003e\n\t\u003cli\u003eSince it's also one piece of global state, you can easily forget to set\n\tone of the required fields for a group type. The resulting danmaku group\n\tthen reuses these values from the last time they were set… which might have\n\tbeen as far back as another boss fight from a previous stage.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e And of course, I wouldn't point this out if it\n\tdidn't actually happen in Shinki's pattern code. Twice.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tDeclaring a separate structure instance with the static data for every\n\tpattern would be both safer \u003ci\u003eand\u003c/i\u003e more space-efficient, and there's\n\tmore than enough space left for that in the game's data segment.\u003cbr\u003e\n\tBut all in all, the pattern functions are short, sweet, and easy to follow.\n\tThe \u003ca href=\"https://www.youtube.com/watch?v=W8UHzPi4K7c\u0026t=136s\"\u003e\"devil\"\n\tpattern\u003c/a\u003e \u003ci\u003eis\u003c/i\u003e significantly more complex than the others, but still\n\tfar from TH01's final bosses at their worst. I especially like the clear\n\tarchitectural separation between \"one-shot pattern\" functions that return\n\t\u003ccode\u003etrue\u003c/code\u003e once they're done, and \"looping pattern\" functions that\n\trun as long as they're being called from a boss's main function. Not many\n\tall too interesting things in these pattern functions for the most part,\n\texcept for two pieces of evidence that Shinki was coded after Yumeko:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe gather animation function in the first two phases contains a bullet\n\tgroup configuration that \u003ci\u003elooks\u003c/i\u003e like it's part of an unused danmaku\n\tpattern. It quickly turns out to just be copy-pasted from a similar function\n\tin Yumeko's fight though, where it \u003ci\u003eis\u003c/i\u003e turned into actual\n\tbullets.\u003c/li\u003e\n\t\u003cli\u003eAs one of the two places where ZUN forgot to set a template field, the\n\tlasers at the end of the white wing preparation pattern reuse the 6-pixel\n\twidth of Yumeko's final laser pattern. This actually has an effect on\n\tgameplay: Since these lasers are active for the first 8 frames after\n\tShinki's wings appear on screen, the player can get hit by them in the last\n\t2 frames after they grew to their final width.\n\t\t\u003cfigure style=\"width: 384px\"\u003e\n\t\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-30-TH05-Shinki-wing-preparation-lasers.webp?589b05c7\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"193\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-30-TH05-Shinki-wing-preparation-lasers.avi?51fadde9\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-30-TH05-Shinki-wing-preparation-lasers.webm?0ee9c9bd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-30-TH05-Shinki-wing-preparation-lasers.webm?84153a0d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-30-TH05-Shinki-wing-preparation-lasers.webm?9e48f89f\" type=\"video/webm\"\u003eVideo of the lasers at the end of TH05 Shinki's wing preparation pattern, demonstrating that they can kill the player in their last 2 frames. \u003ca href=\"/blog/static/video/zmbv/2022-04-30-TH05-Shinki-wing-preparation-lasers.avi?51fadde9\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\t\u003cfigcaption\u003eOf course, there are more than enough safespots \u003ci\u003ebetween\u003c/i\u003e the lasers.\u003c/figcaption\u003e\n\t\t\u003c/figure\u003e\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tSpeaking about that wing sprite: If you look at \u003ccode\u003eST05.BB2\u003c/code\u003e (or\n\tany other file with a large sprite, for that matter), you notice a rather\n\tweird file layout:\n\u003c/p\u003e\u003cfigure class=\"pixelated checkerboard\"\u003e\n\t\u003cimg\n\tsrc=\"/blog/static/2022-04-30-TH05-ST05.BB2.png?776e3f3c\"\n\talt=\"Raw file layout of TH05's ST05.BB2, demonstrating master.lib's supposed BFNT width limit of 64 pixels\"\n\tstyle=\"height: 384px;\"\u003e\n\t\u003cfigcaption\u003eA large sprite split into multiple smaller ones with a width of\n\t64 pixels each? What's this, hardware sprite limitations? On \u003ci\u003emy\u003c/i\u003e\n\tPC-98?!\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAnd it's not a limitation of the sprite width field in the BFNT+ header\n\teither. Instead, it's master.lib's BFNT functions which are limited to\n\tsprite widths up to 64 pixels… or at least that's what\n\t\u003ccode\u003eMASTER.MAN\u003c/code\u003e claims. Whatever the restriction was, it seems to be\n\tcompletely nonexistent as of master.lib version 0.23, and none of the\n\tmaster.lib functions used by the games have any issues with larger\n\tsprites.\u003cbr\u003e\n\tSince ZUN stuck to the supposed 64-pixel width limit though, it's now the\n\t\u003ci\u003egame\u003c/i\u003e that expects Shinki's winged form to consist of 4 physical\n\tsprites, not just 1. Any conversion from another, more logical sprite sheet\n\tlayout back into BFNT+ must therefore replicate the original number of\n\tsprites. Otherwise, the sequential IDs (\"patnums\") assigned to every newly\n\tloaded sprite no longer match ZUN's hardcoded IDs, causing the game to\n\tcrash. This is exactly what used to happen with \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e's\n\t\u003ca href=\"http://lunarcast.net/mystictk.php\"\u003eMysticTK automation scripts\u003c/a\u003e,\n\twhich combined these exact sprites into a single large one. This issue has\n\tnow been fixed – just in case there are some underground modders out there\n\twho used these scripts and wonder why their game crashed as soon as the\n\tShinki fight started.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd then the code quality takes a nosedive with Shinki's main function.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Even in TH05, these boss and midboss update\n\tfunctions are still very imperative:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe origin point of all bullet types used by a boss must be manually set\n\tto the current boss/midboss position; there is no concept of a bullet type\n\ttracking a certain entity.\u003c/li\u003e\n\t\u003cli\u003eThe same is true for the target point of a player's homing shots…\u003c/li\u003e\n\t\u003cli\u003e… and updating the HP bar. At least the initial fill animation is\n\tabstracted away rather decently.\u003c/li\u003e\n\t\u003cli\u003eIncrementing the phase frame variable also must be done manually. TH05\n\teven \"innovates\" here by giving the boss update function exclusive ownership\n\tof that variable, in contrast to TH04 where that ownership is given out to\n\tthe player shot collision detection (?!) and boss defeat helper\n\tfunctions.\u003c/li\u003e\n\t\u003cli\u003eSpeaking about collision detection: That is done by calling different\n\tfunctions depending on whether the boss is supposed to be invincible or\n\tnot.\u003c/li\u003e\n\t\u003cli\u003eTimeout conditions? No standard way either, and all done with manual\n\t\u003ccode\u003eif\u003c/code\u003e statements. In combination with the regular phase end\n\tcondition of lowering (mid)boss HP to a certain value, this leads to quite a\n\tconvoluted control flow.\u003c/li\u003e\n\t\u003cli\u003eThe manual calls to the score bonus functions for cleared phases at least provide some sense of orientation. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eOne potentially nice aspect of all this imperative freedom is that\n\tphases can end outside of HP boundaries… by manually incrementing the\n\tphase variable and resetting the phase \u003ci\u003eframe\u003c/i\u003e variable to 0.\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe biggest WTF in there, however, goes to using one of the 16 state bytes\n\tas a \"relative phase\" variable for differentiating between boss phases that\n\tshare the same branch within the \u003ccode\u003eswitch(boss.phase)\u003c/code\u003e\n\tstatement. While it's commendable that ZUN tried to reduce code duplication\n\tfor once, he could have just branched depending on the actual\n\t\u003ccode\u003eboss.phase\u003c/code\u003e variable? The same state byte is then reused in the\n\t\"devil\" pattern to track the activity state of the big jerky lasers in the\n\tsecond half of the pattern. If you somehow managed to end the phase after\n\tthe first few bullets of the pattern, but before these lasers are up,\n\tShinki's update function would think that you're still in the phase\n\t\u003ci\u003ebefore\u003c/i\u003e the \"devil\" pattern. The main function then sequence-breaks\n\tright to the defeat phase, skipping the final pattern with the burning Makai\n\tbackground. Luckily, the HP boundaries are far away enough to make this\n\timpossible in practice.\u003cbr\u003e\n\tThe takeaway here: If you \u003ci\u003ewant\u003c/i\u003e to use the state bytes for your custom\n\tboss script mods, alias them to your own 16-byte structure, and limit each\n\tof the bytes to a clearly defined meaning across your entire boss script.\n\u003c/p\u003e\u003cp\u003e\n\tOne final discovery that doesn't seem to be documented anywhere yet: Shinki\n\tactually has a hidden bomb shield during her two purple-wing phases.\n\tuth05win got this part slightly wrong though: It's not a \u003ci\u003ecomplete\u003c/i\u003e\n\tshield, and hitting Shinki will still deal 1 point of chip damage per\n\tframe. For comparison, the first phase lasts for 3,000 HP, and the \"devil\"\n\tpattern phase lasts for 5,800 HP.\n\u003c/p\u003e\u003cp\u003e\n\tAnd there we go, 3rd PC-98 Touhou \u003cspan class=\"hovertext\" title=\"The foreground sprite rendering is still in ASM, but that's 100% boilerplate code.\"\u003eboss\n\tscript*\u003c/span\u003e decompiled, 28 to go! 🎉 In case you were expecting a fix for\n\tthe \u003ca href=\"https://youtu.be/b1k82w1VzUc\"\u003eShinki death glitch\u003c/a\u003e: That one\n\tis more appropriately fixed as part of the Mai \u0026 Yuki script. It also\n\trequires new code, should ideally look a bit prettier than just removing\n\tcheetos between one frame and the next, and I'd still like it to fit within\n\tthe original position-dependent code layout… Let's do that some other\n\ttime.\u003cbr\u003e\n\tNot much to say about the Stage 1 midboss, or midbosses in general even,\n\texcept that their update functions have to imperatively handle even more\n\tsubsystems, due to the relative lack of helper functions.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe remaining ¾ of the third push went to a bunch of smaller RE and\n\tfinalization work that would have hardly got any attention otherwise, to\n\thelp secure that 50% RE mark. The nicest piece of code in there shows off\n\twhat looks like the optimal way of setting up the\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 GRCG tile register\u003c/a\u003e for monochrome blitting\n\tin a variable color:\n\u003c/p\u003e\u003cpre\u003emov ah, palette_index ; Any other non-AL 8-bit register works too.\n                      ; (x86 only supports AL as the source operand for OUTs.)\n\nrept 4                ; For all 4 bitplanes…\n    shr ah,  1        ; Shift the next color bit into the x86 carry flag\n    sbb al,  al       ; Extend the carry flag to a full byte\n                      ; (CF=0 → 0x00, CF=1 → 0xFF)\n    out 7Eh, al       ; Write AL to the GRCG tile register\nendm\n\u003c/pre\u003e\u003cp\u003e\n\tThanks to Turbo C++'s inlining capabilities, the loop body even decompiles\n\tinto a surprisingly nice one-liner. What a beautiful micro-optimization, at\n\ta place where micro-optimization doesn't hurt and is almost expected.\u003cbr\u003e\n\tUnfortunately, the micro-optimizations went all downhill from there,\n\tbecoming increasingly dumb and undecompilable. Was it really necessary to\n\tsave 4 x86 instructions in the highly unlikely case of a new spark sprite\n\t\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhQAAIAPABAOy8qv///yH5BAUKAAEALAAAAABAAAgAAAJIjGGJye28nnygxuMu0hOrwE2h1yyRCZHeqaJgplhnVV6HDdIQrr/5LavxhsIMceVqqXwhlu/zZC6jTtIIBqV2rtFS5zsF53QFADs=\"\n\t\u003e being spawned outside the playfield? That one 2D polar→Cartesian\n\tconversion function then pointed out Turbo C++ 4.0J's woefully limited\n\tsupport for 32-bit micro-optimizations. The code generation for 32-bit\n\t\u003ca href=\"/blog/2022-02-18\"\u003e📝 pseudo-registers\u003c/a\u003e is so bad that they almost\n\taren't worth using for arithmetic operations, and the inline assembler just\n\tflat out doesn't support anything 32-bit. No use in decompiling a function\n\tthat you'd have to entirely spell out in machine code, especially if the\n\tsame function already exists in multiple other, more idiomatic C++\n\tvariations.\u003cbr\u003e\n\tRounding out the third push, we got the TH04/TH05 \u003ccode\u003eDEMO?.REC\u003c/code\u003e\n\treplay file reading code, which should finally prove that nothing about the\n\tgame's original replay system could serve as even just the foundation for\n\tcommunity-usable replays. Just in case anyone was still thinking that.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNext up: Back to TH01, with the Elis fight! Got a bit of room left in the\n\tcap again, and there are \u003ci\u003ea lot\u003c/i\u003e of things that would make a lot of\n\tsense now:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eTH04 would really enjoy a large number of dedicated pushes to catch up\n\twith TH05. This would greatly support the finalization of both games.\u003c/li\u003e\n\t\u003cli\u003eContinuing with TH05's bosses and midbosses has shown to be good value\n\tfor your money. Shinki would have taken even less than 2 pushes if she\n\thadn't been the first boss I looked at.\u003c/li\u003e\n\t\u003cli\u003eI've got a new idea for\n\t\u003ca href=\"/blog/2020-09-07\"\u003e📝 properly linking in master.lib and getting rid of the 32-bit build step\u003c/a\u003e…\n\t\u003csmall\u003e(\u003cstrong\u003eEdit (2022-05-31):\u003c/strong\u003e Nope, that didn't work out\n\tafter all.)\u003c/small\u003e\u003c/li\u003e\n\t\u003cli\u003eOh, and I also added Seihou as a selectable goal, for the two people out\n\tthere who genuinely like it. If I ever want to quit my day job, I need to\n\tbranch out into safer territory that isn't threatened by takedowns, after\n\tall.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-05-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-04-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-04-30T22:58:07Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-04-18",
      "url": "https://rec98.nmlgc.net/blog/2022-04-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-04-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-03-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-04-18\"\u003e\u003ctime datetime=\"2022-04-18T20:41:01Z\"\u003e2022-04-18 20:41\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0189\"\u003eP0189\u003c/a\u003e\n\t\t\tTH04 RE (Kurumi + Stage 4 Marisa crashes) / TH05 decompilation (Stage 1-5 boss backgrounds)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/22abdd1...b4876b6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e, \u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kurumi\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 2 boss.\"\u003ekurumi\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/marisa-4\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 4 boss, when playing as Reimu.\"\u003emarisa-4\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 1 boss.\"\u003esara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\n\n\n\u003cp\u003e\n\t(Before we start: Make sure you've read the \u003ca\n\thref=\"/faq#tsa-takedown\"\u003ecurrent version of the FAQ section on a potential\n\ttakedown of this project\u003c/a\u003e, updated in light of the recent DMCA claims\n\tagainst PC-98 Touhou game downloads.)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSlight change of plans, because we got \u003ca\n\thref=\"https://github.com/nmlgc/rec98.nmlgc.net/issues/2\"\u003einstructions for\n\treliably reproducing the TH04 Kurumi Divide Error crash\u003c/a\u003e! Major thanks to\n\tColin Douglas Howell. With those, it also made sense to immediately look at\n\tthe crash in the Stage 4 Marisa fight as well. This way, I could release\n\tboth of the obligatory bugfix mods at the same time.\u003cbr\u003e\n\tEspecially since it turned out that I was wrong: Both crashes are entirely\n\tunrelated to the custom entity structure that would have required PI-centric\n\tprogress. They are completely specific to Kurumi's and Marisa's\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/danmaku-pattern\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/a\u003e\u003c/span\u003e code, and really are two separate bugs\n\twith no connection to each other. All of the necessary research nicely fit\n\tinto \u003ca class=\"customer\" href=\"https://twitter.com/TheArandui\"\u003eArandui\u003c/a\u003e's 0.5 pushes, with no further deep understanding\n\trequired here.\n\u003c/p\u003e\u003cp\u003e\n\tBut why were there still three weeks between Colin's message and this blog\n\tpost? DMCA distractions aside: There are no easy fixes this time, unlike\n\t\u003ca href=\"/blog/2021-11-29\"\u003e📝 back when I looked at the Stage 5 Yuuka \tcrash\u003c/a\u003e.\n\tJust like how division by zero is undefined in mathematics, it's also,\n\tliterally, undefined what should happen instead of these two\n\t\u003ccode\u003eDivide error\u003c/code\u003e crashes. This means that \u003ci\u003eany possible \"fix\" can\n\tonly ever be a fanfiction interpretation of the intentions behind ZUN's\n\tcode. The \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e community should be aware of this, and\n\tmight decide to handle these cases differently.\u003c/i\u003e And if we\n\t\u003ci\u003ehave\u003c/i\u003e to go into fanfiction territory to work around crashes in the\n\tcanon games, we'd better document what exactly we're fixing here and how, as\n\tcomprehensible as possible.\n\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003ca href=\"#kurumi-2022-04-18\"\u003eKurumi's crash\u003c/a\u003e \u003c/li\u003e\u003cli\u003e\u003ca href=\"#marisa-2022-04-18\"\u003eMarisa's crash\u003c/a\u003e \u003c/li\u003e\u003c/ol\u003e\u003chr id=\"kurumi-2022-04-18\"\u003e\u003cp\u003e\n\tWith that out of the way, let's look at Kurumi's crash first, since it's way\n\teasier to grasp. This one is known to primarily happen to new players, and\n\tit's easy to see why:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eIn one of the patterns in her third phase, Kurumi fires a series of 3\n\taimed rings from both edges of the playfield. By default (that is, on Normal\n\tand with regular rank), these are 6-way rings.\u003c/li\u003e\n\t\u003cli\u003e6 happens to be quite a peculiar number here, due to how rings are\n\t(manually) tuned based on the current \"rank\" value (\u003ccode\u003eplayperf\u003c/code\u003e)\n\tbefore being fired. The code, abbreviated for clarity:\n\t\u003cpre\u003eif(bullets_in_ring \u003e= 5) {\n\tif(playperf \u0026lt;= 10) {\n\t\tbullets_in_ring -= 2;\n\t}\n\tif(playperf \u0026lt;= 4) {\n\t\tbullets_in_ring -= 4;\n\t}\n}\u003c/pre\u003e\u003c/li\u003e\n\t\u003cli\u003eLet's look at the range of possible \u003ccode\u003eplayperf\u003c/code\u003e values per\n\tdifficulty level: \u003ctable class=\"numbers\"\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003eEasy\u003c/th\u003e\n\t\t\t\u003cth\u003eNormal\u003c/th\u003e\n\t\t\t\u003cth\u003eHard\u003c/th\u003e\n\t\t\t\u003cth\u003eLunatic\u003c/th\u003e\n\t\t\t\u003cth\u003eExtra\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003eplayperf_min\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003e\u003cstrong\u003e4\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003ctd\u003e11\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\t\u003ctd\u003e22\u003c/td\u003e\n\t\t\t\u003ctd\u003e16\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003ccode\u003eplayperf_max\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003ctd\u003e16\u003c/td\u003e\n\t\t\t\u003ctd\u003e24\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e34\u003c/td\u003e\n\t\t\t\u003ctd\u003e20\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/table\u003e\n\t\u003cp\u003e\u003csmall\u003e\u003cstrong\u003eEdit (2022-05-24):\u003c/strong\u003e This blog post initially had\n\t26 instead of 16 for \u003ccode\u003eplayperf_min\u003c/code\u003e for the Extra Stage. Thanks\n\tto Popfan for pointing out that typo!\u003c/small\u003e\u003c/p\u003e\u003c/li\u003e\n\t\u003cli\u003eReducing rank to its minimum on Easy mode will therefore result in a\n\t0-ring after tuning.\u003c/li\u003e\n\t\u003cli\u003eTo calculate the individual angles of each bullet in a ring, ZUN divides\n\t360° (or, more correctly,\n\t\u003ca href=\"/blog/2022-03-05\"\u003e📝 \u003ccode\u003e0x100\u003c/code\u003e\u003c/a\u003e) by the total number of\n\tbullets…\u003c/li\u003e\n\t\u003cli\u003eBoom, division by zero.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.webp?c31b60e6\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = 4\u003c/code\u003e (minimum rank on Easy mode)\" data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"80\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.avi?b50e2e44\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.webm?7e478f4f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.webm?6dc8632a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.webm?4dacfca3\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-pattern-playperf-4.avi?b50e2e44\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.webp?79d31f92\" preload=\"none\" controls data-title=\"\u003ccode\u003eplayperf = 16\u003c/code\u003e (starting rank, and maximum on Easy mode)\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"200\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.avi?ed6ce0fe\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.webm?aee2ad20\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.webm?59fcc785\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.webm?036d22d4\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-pattern-playperf-16.avi?ed6ce0fe\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe pattern that causes the crash in Kurumi's fight. Also\n\t\tdemonstrates how the number of bullets in a ring is always halved on\n\t\tEasy Mode after the rank-based tuning, leading to just a 3-ring on\n\t\t\u003ccode\u003eplayperf = 16\u003c/code\u003e.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tSo, what should the workaround look like? Obviously, we want to modify\n\tneither the default number of ring bullets nor the tuning algorithm – that\n\twould change all other non-crashing variations of this pattern on other\n\tdifficulties and ranks, creating a fork of the original gameplay. Instead, I\n\tcame up with four possible workarounds that all seemed somewhat logical to\n\tme:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eFiring no bullet, i.e., interpreting \u003ci\u003e0-ring\u003c/i\u003e literally. This would\n\tcreate the only constellation in which a call to the bullet group spawn\n\tfunctions would not spawn at least one new bullet.\u003c/li\u003e\n\t\u003cli\u003eFiring a \"1-ring\", i.e., a single bullet. This would be consistent with\n\thow the bullet spawn functions behave for \"0-way\" stack and spread\n\tgroups.\u003c/li\u003e\n\t\u003cli\u003eFiring a \"∞-ring\", i.e., 200 bullets, which is as much as the game's cap\n\ton 16×16 bullets would allow. This would poke fun at the whole \"division by\n\tzero\" idea… but given that we're still talking about Easy Mode (and\n\tespecially new players) here, it might be a tad too cruel. Certainly the\n\tmost trollish interpretation.\u003c/li\u003e\n\t\u003cli\u003eTriggering an immediate Game Over, exchanging the hard crash for a\n\tsofter and more controlled shutdown. Certainly the option that would be\n\tclosest to the behavior of the original games, and perhaps the only one to\n\tbe accepted in Serious, High-Level Play™.\u003c/li\u003e\n\u003c/ol\u003e\u003cfigure\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.webp?f9b7fb6a\" preload=\"none\" controls data-title=\"0 bullets\" loop data-active width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"200\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.avi?43444e23\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.webm?0573c1d3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.webm?d131f5dc\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.webm?218a2778\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-0-bullets.avi?43444e23\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.webp?50790be7\" preload=\"none\" controls data-title=\"1 bullet\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"200\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.avi?d4409fed\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.webm?9ccc8b79\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.webm?984443d0\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.webm?b8725832\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-1-bullet.avi?d4409fed\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.webp?629d541e\" preload=\"none\" controls data-title=\"200 bullets\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"200\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.avi?ce59a0eb\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.webm?3a3bcb45\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.webm?90a26afd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.webm?f38c6dcd\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-200-bullets.avi?ce59a0eb\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Kurumi-crash-workaround-gameover.webp?e467a474\" preload=\"none\" controls data-title=\"Game Over\" loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"200\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-gameover.avi?93e02c91\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Kurumi-crash-workaround-gameover.webm?b44c85a0\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Kurumi-crash-workaround-gameover.webm?e060afc7\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Kurumi-crash-workaround-gameover.webm?aeb9fe61\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Kurumi-crash-workaround-gameover.avi?93e02c91\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAs I was writing this post, it felt increasingly wrong for me to make this\n\tdecision. So I once again went to Twitter, where \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1512941767162798090\"\u003e56.3%\n\tvoted in favor of the 1-bullet option\u003c/a\u003e. Good that I asked! I myself was\n\tmore leaning towards the 0-bullet interpretation, which only got 28.7% of\n\tthe vote. Also interesting are the 2.3% in favor of the Game Over option but\n\tI get it, low-rank Easy Mode isn't exactly the most competitive mode of\n\tplaying TH04.\u003cbr\u003e\n\tThere are reports of Kurumi crashing on higher difficulties as well, but I\n\tcould verify none of them. If they aren't fixed by this workaround, they're\n\tcaused by an entirely different bug that we have yet to discover.\n\u003c/p\u003e\u003chr id=\"marisa-2022-04-18\"\u003e\u003cp\u003e\n\tOnto the Stage 4 Marisa crash then, which \u003ci\u003edoes\u003c/i\u003e in fact apply to all\n\tdifficulty levels. I was also wrong on this one – it's a hell of a lot more\n\tintricate than being just a division by the number of on-screen bits.\n\tWithout having decompiled the entire fight, I can't give a completely\n\taccurate picture of what happens there yet, but here's the rough idea:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eMarisa uses different patterns, depending on whether at least one of her\n\tbits \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAIkAAP1ERfzs7f///yH5BAUKAAMALAAAAAAgACAAAAKjnI8my5sP0Qq0UhfzpKD7imXHFnjmdwniIFjnC1hq1HLwDUJ1eeN5UusJf6OKsCeT2Hy8U1JhxG2clsBs1zSlSs3qhVV9gbBeGTlrbTDK5nInpT5XSd7YmO2l49P7dn+LwqZXFzNBlScnZniYRAbD9/jkGMn0BbV0xDhzmZUZaAUU5Rm4WYTp+RR6ekT0sIPaqvNaGbJColm6MiKXpisSVxtRAAA7\"\n\t\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAHQAdMxUzfzs7f///yH5BAUKAAMALAAAAAAgACAAAAKnnI8my5sP0Qq0UhfzpKD7imXHFnjmdwniIFjnC1hq1HLwDUJ1eeN5UusJf6OKsCeT2Hw8kyUwYxlxjGbsCVW4XikbNrdbfhqkb/fpBFXNavaVXP7GsQ1o2Dx/1qpXdl4e1AfI5sWHQncn1tF1Iie1xdUCo6cFeSI5mVRp1YhE9Mh5dJilpCjaF1VkeqQJNHWKqvE69Kmz45myMnKblqsLlOj7awunWwAAOw==\"\n\t\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAAAB3ZiX/Pzs7f///yH5BAUKAAMALAAAAAAgACAAAAKknI8my5sP0Qq0UhfzpKD7imXHFnjmdwniIFjnC1hq1HLwDUJ1eeN5UusJf6OKsCeT2I4vS2DGMsKcS+pT4To9n61WzHrdLT8NEphL1ZbFYLPz2+2eQWznmjFH57nBvLu90TH398bnhUJF2ISmlRiVtTjR+MM2xGNiVzTGhHilecnZCYW1yUn0GSqqIxV6CsRqebFCgkS3MrIzyXX7QLjLS3N3WwAAOw==\"\n\t\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPEDAABkACCoH/zs7f///yH5BAUKAAMALAAAAAAgACAAAAKonI8my5sP0Qq0UhfzpKD7imXHFnjmdwniIFjnC1hq1HLwDUJ1eeN5UusJf6OKsCeT2I4vS2DGMsJsy5jzqXC9NhPrFburdp410jd1NYHIZ6f52oiH4XM3l9FG59nPvX7fwJH31haIQvd1wteiBhel9bHDqJj0KIXyMVVpyaR5oSTW6YUFyiM69gl0KbpZeuoF9bDT2aoz65OyMnJ7SKYrW5f7SyMXG1EAADs=\"\n\t\u003e is still alive, or all of them have been destroyed.\u003c/li\u003e\n\t\u003cli\u003eDestroying the last bit will immediately switch to the bit-less\n\tcounterpart of the current pattern.\u003c/li\u003e\n\t\u003cli\u003eThe bits won't respawn before the pattern ended, which ensures that the\n\tbit-less version is always shown in its entirety after being started or\n\tswitched into.\u003c/li\u003e\n\t\u003cli\u003eIn two of the bit-less patterns, Marisa gradually moves to the point\n\treflection of her position at the start of the pattern across the playfield\n\tcoordinate of (﻿192,\u0026nbsp;112﻿), or (﻿224,\u0026nbsp;128﻿) on screen.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure class=\"singleplayer_playfield\"\u003e\n\t\u003cimg src=\"/blog/static/2022-04-18-TH04-Marisa4-point-reflection.png?9327e868\"\u003e\n\t\u003cfigcaption\u003eReference points for Marisa's point-reflected movement. Cyan:\n\tMarisa's position, green: (﻿192,\u0026nbsp;112﻿), yellow: the intended end\n\tpoint.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cul\u003e\n\t\u003cli\u003eThe velocity of this movement is determined by both her distance to that\n\tpoint and the total amount of frames that this instance of the bit-less\n\tpattern will last.\u003c/li\u003e\n\t\u003cli\u003eSince this frame amount is directly tied to the frame the player\n\tdestroyed the last bit on, it becomes a user-controlled variable. I think\n\tyou can see where this is going…\u003c/li\u003e\n\t\u003cli\u003eThe last 12 frames of this duration, however, are always reserved for a\n\t\"braking phase\", where Marisa's velocity is halved on each frame.\u003c/li\u003e\n\t\u003cli\u003ePutting it all together, we get this formula:\n\t\u003cpre\u003eboss_velocity.x = ((192 - boss_position.x) / ((duration / 2) - (12 / 2)));\nboss_velocity.y = ((112 - boss_position.y) / ((duration / 2) - (12 / 2)));\u003c/pre\u003e\u003c/li\u003e\n\t\u003cli\u003eSet \u003ccode\u003eduration\u003c/code\u003e to 12 or 13, and boom, \u003ccode\u003eDivide\n\terror\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eThis part of the code only runs every 4 frames though. This expands the\n\ttime window for this crash to 4 frames, rather than just the two frames you\n\twould expect from looking at the division itself.\u003c/li\u003e\n\t\u003cli\u003eBoth of the broken patterns run for a maximum of 160 frames. Therefore,\n\tthe crash will occur when Marisa's last bit is destroyed between frame 152\n\tand 155 inclusive. On these frames, the\n\t\u003ccode\u003elast_frame_with_bits_alive\u003c/code\u003e variable is set to 148, which is the\n\tcrucial 12 \u003ccode\u003eduration\u003c/code\u003e frames away from the maximum of 160.\u003c/li\u003e\n\t\u003cli\u003eInterestingly enough, the calculated velocity is \u003ci\u003ealso\u003c/i\u003e only\n\tapplied every 4 frames, with Marisa actually staying still for the 3 frames\n\tinbetween. As a result, she either moves\u003cul\u003e\n\t\t\u003cli\u003etoo slowly to ever actually reach the yellow point if the last bit\n\t\twas destroyed early in the pattern (see destruction frames 68 or\n\t\t112),\u003c/li\u003e\n\t\t\u003cli\u003eor way too quickly, and almost in a jerky, teleporting way (see\n\t\tdestruction frames 144 or 148).\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eFinally, as you may have already gathered from the formula: Destroying\n\tthe last bit between frame 156 and 160 inclusive results in\n\t\u003ccode\u003eduration\u003c/code\u003e values of 8 or 4. These actually push Marisa\n\t\u003ci\u003eaway\u003c/i\u003e from the intended point, as the divisor becomes negative.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.webp?112caf49\" preload=\"none\" controls data-title=\"Frame 68\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.avi?8277d47b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.webm?d597d154\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.webm?2059c96b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.webm?da17462b\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-68.avi?8277d47b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"37\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.webp?e8773034\" preload=\"none\" controls data-title=\"Frame 112\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.avi?cd3c255e\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.webm?aaf8550d\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.webm?6bcb03df\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.webm?f9391409\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-112.avi?cd3c255e\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"81\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.webp?50996bc0\" preload=\"none\" controls data-title=\"Frame 144\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.avi?8064eb39\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.webm?66b386f9\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.webm?a31fbc19\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.webm?5a3a6b6c\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-144.avi?8064eb39\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"113\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.webp?64a0d3fe\" preload=\"none\" controls data-title=\"Frame 148\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.avi?51be1c9f\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.webm?584735cb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.webm?60e73a78\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.webm?a6122ca8\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-148.avi?51be1c9f\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"117\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.webp?8faa5429\" preload=\"none\" controls data-title=\"Frame 152 (Divide Error)\" data-active width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"122\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.avi?aa9874aa\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.webm?fb75dace\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.webm?2d2fb06f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.webm?8f9a27c3\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-152.avi?aa9874aa\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"121\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.webp?c2ba518f\" preload=\"none\" controls data-title=\"Frame 156\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.avi?476c1af1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.webm?89075985\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.webm?02acc82e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.webm?2a2d2238\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-156.avi?476c1af1\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"125\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.webp?08312960\" preload=\"none\" controls data-title=\"Frame 160\" loop width=\"640\" height=\"400\" data-fps=\"20\" data-frame-count=\"170\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.avi?fcca5a95\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.webm?8f602a00\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.webm?9289527c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.webm?fe19a3d4\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-pattern-bits-160.avi?fcca5a95\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"129\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tOne of the two patterns in TH04's Stage 4 Marisa boss fight that feature\n\t\tframe number-dependent point-reflected movement. The bits were hacked to\n\t\tself-destruct on the respective frame.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\ttl;dr: \"Game crashes if last bit destroyed within 4-frame window near end of\n\ttwo patterns\". For an informed decision on a new movement behavior for these\n\tlast 8 frames, we definitely need to know all the details behind the crash\n\tthough. Here's what I would interpret into the code:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eNot moving at all, i.e., interpreting 0 as the middle ground between\n\tpositive and negative movement. This would also make sense because a\n\t12-frame \u003ccode\u003eduration\u003c/code\u003e implies 100% of the movement to consist of\n\tthe braking phase – and Marisa wasn't moving before, after all.\u003c/li\u003e\n\t\u003cli\u003eMove at maximum speed, i.e., dividing by 1 rather than 0. Since the\n\tmovement duration is still 12 in this case, Marisa will immediately start\n\tbraking. In total, she will move exactly ¾ of the way from her initial\n\tposition to (﻿192,\u0026nbsp;112﻿) within the 8 frames before the pattern\n\tends.\u003c/li\u003e\n\t\u003cli\u003eDirectly warping to (﻿192,\u0026nbsp;112﻿) on frame 0, and to the\n\tpoint-reflected target on 4, respectively. This \"emulates\" the division by\n\tzero by moving Marisa at infinite speed to the exact two points indicated by\n\tthe velocity formula. It also fits nicely into the 8 frames we have to fill\n\there. Sure, Marisa can't reach these points at any other duration, but why\n\t\u003ci\u003eshouldn't\u003c/i\u003e she be able to, with infinite speed? Then again, if Marisa\n\tis far away enough from (﻿192,\u0026nbsp;112﻿), this workaround would warp her\n\tacross the entire playfield. \u003ci\u003eCan\u003c/i\u003e Marisa teleport according to lore? I\n\thave no idea… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eTriggering an immediate Game O– hell no, this is the Stage 4 boss,\n\tpeople \u003ci\u003ealready\u003c/i\u003e hate losing runs to this bug!\u003c/li\u003e\n\u003c/ol\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player with-markers\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-workaround-still.webp?76311dec\" preload=\"none\" controls data-title=\"Not moving at all\" loop data-active width=\"384\" height=\"368\" data-fps=\"20\" data-frame-count=\"70\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-still.avi?79c50794\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-workaround-still.webm?1bf27c81\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-workaround-still.webm?4a07de61\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-workaround-still.webm?195a5c1a\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-still.avi?79c50794\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"21\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-workaround-move.webp?c8c17328\" preload=\"none\" controls data-title=\"Move and brake\" loop width=\"384\" height=\"368\" data-fps=\"20\" data-frame-count=\"70\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-move.avi?308d488b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-workaround-move.webm?20b8a40b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-workaround-move.webm?e1116fdb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-workaround-move.webm?c349632f\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-move.avi?308d488b\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"21\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH04-Marisa4-crash-workaround-warp.webp?6b40d067\" preload=\"none\" controls data-title=\"Warp\" loop width=\"384\" height=\"368\" data-fps=\"20\" data-frame-count=\"70\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-warp.avi?7ba5f451\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH04-Marisa4-crash-workaround-warp.webm?f51d271e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH04-Marisa4-crash-workaround-warp.webm?3f929cd2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH04-Marisa4-crash-workaround-warp.webm?3b5687c0\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH04-Marisa4-crash-workaround-warp.avi?7ba5f451\"\u003eDownload\u003c/a\u003e\u003crec98-video-marker data-frame=\"21\" data-title=\"Bit destruction frame\" data-alignment=\"right\"\u003e\u003c/rec98-video-marker\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAsking Twitter worked great for the Kurumi workaround, so let's do it again!\n\tGotta attach a screenshot of an earlier draft of this blog post though,\n\tsince this stuff is impossible to explain in tweets…\n\u003c/p\u003e\u003cp\u003e\n\t…and it \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1515018759228084229\"\u003ewent\n\tthrough the roof, becoming the most successful ReC98 tweet so far\u003c/a\u003e?!\n\tApparently, y'all really like to just look at descriptions of overly complex\n\tbugs that I'd consider way beyond the typical attention span that can be\n\texpected from Twitter. Unfortunately, all those tweet impressions didn't\n\t\u003ci\u003equite\u003c/i\u003e translate into poll turnout. The \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1515018996885688325\"\u003eresults\u003c/a\u003e\n\twere pretty evenly split between 1) and 2), with option 1) just coming out\n\tslightly ahead at 49.1%, compared to 41.5% of option 2).\n\u003c/p\u003e\u003cp\u003e\n\t(And yes, I only noticed after creating the poll that warping to both the\n\tgreen and yellow points made more sense than warping to just one of the two.\n\tLet's hope that this additional variant wouldn't have shifted the results\n\ttoo much. Both warp options only got 9.4% of the vote after all, and no one\n\telse came up with the idea either. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e In the end,\n\tyou can always merge together your preferred combination of workarounds from\n\tthe Git branches linked below.)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo here you go: The new definitive version of TH04, containing not only the\n\tcommunity-chosen Kurumi and Stage 4 Marisa workaround variant, but also the\n\t\u003ca href=\"/blog/2021-11-29\"\u003e📝 No-EMS bugfix from last year\u003c/a\u003e.\n\t\u003cstrong\u003eEdit (2022-05-31): This package is outdated, \u003ca href=\"/blog/2022-05-31\"\u003e📝 the current version is here\u003c/a\u003e!\u003c/strong\u003e\n\t\u003ca class=\"download\" href=\"/blog/static/2022-04-18-community-choice-fixes.zip?7da498c3\" data-kb=\"116.7\"\u003e2022-04-18-community-choice-fixes.zip \u003c/a\u003e\n\tOh, and let's also add \u003ca\n\thref=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e's TH03 GDC clock fix\n\tfrom 2019 because why not. This binary was built from the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/community_choice_fixes\"\u003e\u003ccode\u003ecommunity_choice_fixes\u003c/code\u003e\u003c/a\u003e\n\tbranch, and you can find the code for all the individual workarounds on\n\tthese branches:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_0_ring_ignore\"\u003e\u003ccode\u003eth04_0_ring_ignore\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_0_ring_as_single_bullet\"\u003e\u003ccode\u003eth04_0_ring_as_single_bullet\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_0_ring_as_cap_bullets\"\u003e\u003ccode\u003eth04_0_ring_as_cap_bullets\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_0_ring_as_gameover\"\u003e\u003ccode\u003eth04_0_ring_as_gameover\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_marisa4_crash_still\"\u003e\u003ccode\u003eth04_marisa4_crash_still\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_marisa4_crash_move\"\u003e\u003ccode\u003eth04_marisa4_crash_move\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th04_marisa4_crash_warp\"\u003e\u003ccode\u003eth04_marisa4_crash_warp\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAgain, because it can't be stated often enough: \u003ci\u003eThese fixes are\n\tfanfiction. The \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e community should be aware of\n\tthis, and might decide to handle these cases differently.\u003c/i\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith all of that taking way more time to evaluate and document, this\n\tresearch really had to become part of a proper push, instead of just being\n\tcovered in the quick non-push blog post I initially intended. With ½ of a\n\tpush left at the end, TH05's Stage 1-5 boss background rendering functions\n\tfit in perfectly there. If you wonder how these static backdrop images even\n\tneed any boss-specific code to begin with, you're right – it's basically the\n\tsame function copy-pasted 4 times, differing only in the backdrop image\n\tcoordinates and some other inconsequential details.\u003cbr\u003e\n\tOnly Sara receives a nice variation of the typical\n\t\u003ca href=\"/blog/2021-06-21\"\u003e📝 blocky entrance animation\u003c/a\u003e: The usually\n\topaque bitmap data from \u003ccode\u003eST00.BB\u003c/code\u003e is instead used as a transition\n\tmask from stage tiles to the backdrop image, by making clever use of the\n\ttile invalidation system:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-04-18-TH05-Sara-entrance.webp?e5d5ce21\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"20\" data-frame-count=\"67\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-04-18-TH05-Sara-entrance.avi?83684df1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-04-18-TH05-Sara-entrance.webm?99118bf8\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-04-18-TH05-Sara-entrance.webm?d72cd33c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-04-18-TH05-Sara-entrance.webm?e1108eee\" type=\"video/webm\"\u003eVideo of Sara's entrance animation, showing off .BB files being used as tile invalidation masks. \u003ca href=\"/blog/static/video/zmbv/2022-04-18-TH05-Sara-entrance.avi?83684df1\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tTH04 uses the same effect a bit more frequently, for its first three bosses.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Shinki, for real this time! I've already managed to decompile 10 of\n\ther 11 danmaku patterns within a little more than one push – and yes,\n\t\u003ci\u003ethat one\u003c/i\u003e is included in there. Looks like I've \u003ci\u003eslightly\u003c/i\u003e\n\toverestimated the amount of work required for TH04's and TH05's bosses…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-04-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-03-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-04-18T20:41:01Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-03-27",
      "url": "https://rec98.nmlgc.net/blog/2022-03-27",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-04-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-03-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-03-27\"\u003e\u003ctime datetime=\"2022-03-27T16:13:51Z\"\u003e2022-03-27 16:13\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0186\"\u003eP0186\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Stage transition animation + smaller boss blockers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/a21ab3d...bab5634\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0187\"\u003eP0187\u003c/a\u003e\n\t\t\tTH04 RE (Shared boss state bytes)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/bab5634...426a531\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0188\"\u003eP0188\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Boss defeat sequence / collision + Shinki's 32×32 balls (logic))\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/426a531...e881f95\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous], nrook\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dialog\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The in-game dialog system.\"\u003edialog\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/marisa-4\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 4 boss, when playing as Reimu.\"\u003emarisa-4\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gengetsu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s second Extra Stage boss.\"\u003egengetsu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tDid you know that moving on top of a boss sprite doesn't kill the player in\n\tTH04, only in TH05?\n\u003c/p\u003e\u003cfigure class=\"pixelated singleplayer_playfield\"\u003e\u003cimg\n\tsrc=\"/blog/static/2022-03-27-TH04-No-boss-collision.png?1345fef3\"\n\talt=\"Screenshot of Reimu moving on top of Stage 6 Yuuka, demonstrating the lack of boss↔player collision in TH04\"\u003e\n\t\u003cfigcaption\u003eYup, Reimu is not getting hit… yet.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's the first of only three interesting discoveries in these 3 pushes,\n\tall of which concern TH04. But yeah, 3 for something as seemingly simple as\n\tthese shared boss functions… that's still not quite the speed-up I had hoped\n\tfor. While most of this can be blamed, again, on TH04 and all of its\n\thardcoded complexities, there still was a lot of work to be done on the\n\tmaintenance front as well.  These functions reference a bunch of code I RE'd\n\tyears ago and that still had to be brought up to current standards, with the\n\tdependencies reaching from \u003ca href=\"/blog/2019-03-06\"\u003e📝 boss explosions\u003c/a\u003e\n\tover \u003ca href=\"/blog/2020-05-04\"\u003e📝 text RAM overlay functionality\u003c/a\u003e up to\n\tin-game dialog loading.\n\u003c/p\u003e\u003cp\u003e\n\tThe latter provides a good opportunity to talk a bit about \u003ca\n\thref=\"https://en.wikipedia.org/wiki/X86_memory_segmentation\"\u003ex86 memory\n\tsegmentation\u003c/a\u003e. Many aspiring PC-98 developers these days are very scared\n\tof it, with some even going as far as to rather mess with Protected Mode and\n\tDOS extenders just so that they don't have to deal with it. I wonder where\n\tthat fear comes from… Could it be because every modern programming language\n\tI know of assumes memory to be flat, and lacks any standard language-level\n\tfeatures to even express something like segments and offsets? That's why\n\tcompilers have a hard time targeting 16-bit x86 these days: Doing anything\n\tinteresting on the architecture \u003ci\u003erequires\u003c/i\u003e giving the programmer full\n\tcontrol over segmentation, which \u003ci\u003ealways\u003c/i\u003e comes down to adding the\n\ttypical non-standard language extensions of compilers from back in the day.\n\tAnd as soon as DOS stopped being used, these extensions no longer made sense\n\tand were subsequently removed from newer tools. A good example for this can\n\tbe found in an \u003ca\n\thref=\"https://cs.fit.edu/~mmahoney/cse3101/nasmdoc1.html\"\u003eold version of the\n\tNASM manual\u003c/a\u003e: The project started as an attempt to make x86 assemblers\n\tsimple again by throwing out most of the segmentation features from\n\tMASM-style assemblers, which made complete sense in 1996 when 16-bit DOS and\n\tWindows were already on their way out. But there \u003ci\u003ewas\u003c/i\u003e a point to all\n\tthose features, and that's why ReC98 still has to use the supposedly\n\tinferior TASM.\n\u003c/p\u003e\u003cp\u003e\n\tNot that this fear of segmentation is completely unfounded: All the\n\tsegmentation-related keywords, directives, and \u003ccode\u003e#pragma\u003c/code\u003es\n\tprovided by Borland C++ and TASM absolutely \u003ci\u003ecan\u003c/i\u003e be the cause of many\n\tweird runtime bugs. Even if the compiler or linker catches them, you are\n\toften left with confusing error messages that aged just as poorly as memory\n\tsegmentation itself.\u003cbr\u003e\n\tHowever, embracing the concept does provide quite the opportunity for\n\toptimizations. While it definitely was a very crazy idea, there is a small\n\tbit of brilliance to be gained from making proper use of all these\n\tsegmentation features. Case in point: The buffer for the in-game dialog\n\tscripts in TH04 and TH05.\n\u003c/p\u003e\u003cpre\u003e// Thanks to the semantics of `far` pointers, we only need a single 32-bit\n// pointer variable for the following code.\nextern unsigned char far *dialog_p;\n\n// This master.lib function returns a `void __seg *`, which is a 16-bit\n// segment-only pointer. Converting to a `far *` yields a full segment:offset\n// pointer to offset 0000h of that segment.\ndialog_p = (unsigned char far *)hmem_allocbyte(/* … */);\n\n// Running the dialog script involves pointer arithmetic. On a far pointer,\n// this only affects the 16-bit offset part, complete with overflow at 64 KiB,\n// from FFFFh back to 0000h.\ndialog_p += /* … */;\ndialog_p += /* … */;\ndialog_p += /* … */;\n\n// Since the segment part of the pointer is still identical to the one we\n// allocated above, we can later correctly free the buffer by pulling the\n// segment back out of the pointer.\nhmem_free((void __seg *)dialog_p);\u003c/pre\u003e\u003cp\u003e\n\tIf \u003ccode\u003edialog_p\u003c/code\u003e was a \u003ccode\u003ehuge\u003c/code\u003e pointer, any pointer\n\tarithmetic would have also adjusted the segment part, requiring a second\n\tpointer to store the base address for the \u003ccode\u003ehmem_free\u003c/code\u003e call. Doing\n\tthat will also be necessary for any port to a flat memory model. Depending\n\ton how you look at it, this compression of two logical pointers into a\n\tsingle variable is either quite nice, or really, \u003ci\u003ereally\u003c/i\u003e dumb in its\n\treliance on the precise memory model of one single architecture.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhy look at dialog loading though, wasn't this supposed to be all about\n\tshared boss functions? Well, TH04 unnecessarily puts certain stage-specific\n\tcode into the boss defeat function, such as loading the alternate Stage 5\n\tYuuka defeat dialog before a Bad Ending, or initializing Gengetsu after\n\tMugetsu's defeat in the Extra Stage.\u003cbr\u003e\n\tThat's TH04's second core function with an explicit conditional branch for\n\tGengetsu, after the\n\t\u003ca href=\"/blog/2021-11-29\"\u003e📝 dialog exit code we found last year during EMS research\u003c/a\u003e.\n\tAnd I've heard people say that Shinki was the most hardcoded fight in PC-98\n\tTouhou… Really, Shinki is a perfectly regular boss, who makes proper use of\n\tall internal mechanics in the way they were intended, and doesn't blast\n\tholes into the architecture of the game. Even within TH05, it's Mai and Yuki\n\twho rely on hacks and duplicated code, not Shinki.\n\u003c/p\u003e\u003cp\u003e\n\tThe worst part about this though? How the function distinguishes Mugetsu\n\tfrom Gengetsu. Once again, it uses its own global variable to track whether\n\tit is called the first or the second time within TH04's Extra Stage,\n\tunrelated to the same variable used in the dialog exit function. But this\n\ttime, it's not just any newly created, single-use variable, oh no. In a\n\tmisguided attempt to micro-optimize away a few bytes of conventional memory,\n\tTH04 reserves 16 bytes of \"generic boss state\", which can (and are) freely\n\tused for anything a boss doesn't want to store in a more dedicated\n\tvariable.\u003cbr\u003e\n\tIt might have been worth it if the bosses actually \u003ci\u003eused\u003c/i\u003e most of these\n\t16 bytes, but the majority just use (the same) two, with only Stage 4 Reimu\n\tusing a whopping seven different ones. To reverse-engineer the various uses\n\tof these variables, I pretty much had to map out which of the undecompiled\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/danmaku-pattern\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/a\u003e\u003c/span\u003e functions corresponds to which boss\n\tfight. In the end, I assigned 29 different variable names for each of the\n\tsemantically different use cases, which made up another full push on its\n\town.\n\u003c/p\u003e\u003cp\u003e\n\tNow, 16 bytes of wildly shared state, isn't that the perfect recipe for\n\tbugs? At least during this cursory look, I haven't found any obvious ones\n\tyet. If they do exist, it's more likely that they involve reused state from\n\tearlier bosses – just how the \u003ca\n\thref=\"https://www.youtube.com/watch?v=b1k82w1VzUc\"\u003eShinki death glitch in\n\tTH05 is caused by reusing cheeto data from way back in Stage 4\u003c/a\u003e – and\n\thence require much more boss-specific progress.\u003cbr\u003e\n\tAnd yes, it might have been way too early to look into all these tiny\n\tdetails of specific boss scripts… but then, this happened:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\u003ca\n\thref=\"/blog/static/2022-03-27-TH04-Marisa-4-bit-crash.png?07f32431\"\u003e\u003cimg src=\"/blog/static/2022-03-27-TH04-Marisa-4-bit-crash.png?07f32431\" alt=\"TH04 crashing to the DOS prompt in the Stage 4 Marisa fight, right as the last of her bits is destroyed\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tLooks similar to \u003ca\n\thref=\"https://github.com/spaztron64/th98tuc_site/issues/11\"\u003eanother\n\tscreenshot of a crash in the same fight that was reported in December\u003c/a\u003e,\n\tdoesn't it? I was too much in a hurry to figure it out exactly, but notice\n\thow both crashes happen right as the last of Marisa's four \u003cspan\n\tclass=\"hovertext\" title=\"Yes, ビット is the correct technical term for\n\tthose, according to OMAKE.TXT.\"\u003ebits\u003c/span\u003e is destroyed.\n\t\u003ca class=\"customer\" href=\"https://www.twitch.tv/KirbyComment\"\u003eKirbyComment\u003c/a\u003e has \u003ca\n\thref=\"https://en.touhouwiki.net/wiki/User:KirbyComment/Lotus_Land_Story_Info#Divide_Error_Crash\"\u003esuspected\n\tthis to be the cause for a while\u003c/a\u003e, and now I can pretty much confirm it\n\tto be an unguarded division by the number of on-screen bits in\n\tMarisa-specific pattern code. But what's the cause for Kurumi then?\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAs for fixing it, I can go for either a fast or a slow option:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eSuperficially fixing only this crash will probably just take a fraction\n\tof a push.\u003c/li\u003e\n\t\u003cli\u003eBut I could also go for a deeper understanding by looking at TH04's\n\tversion of the \u003ca href=\"/blog/2020-02-29\"\u003e📝 custom entity structure\u003c/a\u003e. It\n\tnot only stores the data of Marisa's bits, but is also very likely to be\n\tinvolved in Kurumi's crash, \u003ci\u003eand\u003c/i\u003e would get TH04 a lot closer to 100%\n\tPI. Taking that look will probably need at least 2 pushes, and might require\n\tanother 3-4 to completely decompile Marisa's fight, and 2-3 to decompile\n\tKurumi's.\u003c/li\u003e\n\u003c/ol\u003e\u003chr\u003e\u003cp\u003e\n\tOK, now that that's out of the way, time to finish the boss defeat function…\n\tbut not without stumbling over the third of TH04's quirks, relating to the\n\tClear Bonus for the main game or the Extra Stage:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eTo achieve the incremental addition effect for the in-game score display\n\tin the HUD, all new points are first added to a \u003ccode\u003escore_delta\u003c/code\u003e\n\tvariable, which is then added to the actual score at a maximum rate of\n\t61,110 points per frame.\u003c/li\u003e\n\t\u003cli\u003eThere are a fixed 416 frames between showing the score tally and\n\tlaunching into \u003ccode\u003eMAINE.EXE\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eAs a result, TH04's Clear Bonus is effectively limited to\n\t(416\u0026nbsp;×\u0026nbsp;61,110)\u0026nbsp;=\u0026nbsp;25,421,760 points.\u003c/li\u003e\n\t\u003cli\u003eOnly TH05 makes sure to commit the entirety of the\n\t\u003ccode\u003escore_delta\u003c/code\u003e to the actual score before switching binaries,\n\twhich fixes this issue.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd after another few collision-related functions, we're now \u003ci\u003etruly\u003c/i\u003e,\n\tfinally ready to decompile bosses in both TH04 and TH05! Just as the\n\t\u003ci\u003eanything\u003c/i\u003e funds were running out… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e The\n\tremaining ¼ of the third push then went to Shinki's 32×32 ball bullets,\n\trounding out this delivery with a small self-contained piece of the first\n\tTH05 boss we're probably going to look at.\n\u003c/p\u003e\u003cp\u003e\n\tNext up, though: I'm not sure, actually. Both Shinki and Elis seem just a\n\tlittle bit larger than the 2¼ or 4 pushes purchased so far, respectively.\n\tNow that there's a bunch of room left in the cap again, I'll just let the\n\tnext contribution decide – with a preference for Shinki in case of a tie.\n\tAnd if it will take longer than usual for the store to sell out again this\n\ttime (heh), there's still the\n\t\u003ca href=\"/blog/2021-09-12\"\u003e📝 PC-98 text RAM JIS trail word rendering research\u003c/a\u003e\n\twaiting to be documented.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-04-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-03-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-03-27T16:13:51Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-03-05",
      "url": "https://rec98.nmlgc.net/blog/2022-03-05",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-03-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-02-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-03-05\"\u003e\u003ctime datetime=\"2022-03-05T22:55:32Z\"\u003e2022-03-05 22:55\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0184\"\u003eP0184\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Bullets: Special motion and sprite selection + item spawn splash circle)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f9d983e...f918298\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0185\"\u003eP0185\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Curve bullet motion + laser control + playfield shaking)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f918298...a21ab3d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, Blue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/louise\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 2 boss.\"\u003elouise\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTwo years after\n\t\u003ca href=\"/blog/2020-02-16\"\u003e📝 the first look at TH04's and TH05's bullets\u003c/a\u003e,\n\twe finally get to finish their logic code by looking at the special motion\n\ttypes. Bullets as a whole still aren't \u003ci\u003ecompletely\u003c/i\u003e finished as the\n\trendering code is still waiting to be RE'd, but now we've got everything\n\tabout them that's required for decompiling the midboss and boss fights of\n\tthese games.\n\u003c/p\u003e\u003cp\u003e\n\tJust like the motion types of TH01's pellets, the ones we've got here really\n\t\u003ci\u003eare\u003c/i\u003e special enough to warrant an \u003ccode\u003eenum\u003c/code\u003e, despite all the\n\toverlap in the \"slow down and turn\" and \"bounce at certain edges of the\n\tplayfield\" types. Sure, including them in the bitfield I proposed two years\n\tago would have allowed greater variety, but it wouldn't have saved any\n\tmemory. On the contrary: These types use a single global state variable for\n\tthe maximum turn count and delta speed, which a proper customizable\n\tarchitecture would have to integrate into the bullet structure. Maybe it is\n\tpossible to stuff everything into the same amount of bytes, but not without\n\tfirst completely rearchitecting the bullet structure and removing every\n\tsingle piece of redundancy in there. Simply extending the system by adding a\n\tnew \u003ccode\u003eenum\u003c/code\u003e value for a new motion type would be way more\n\tstraightforward for modders.\n\u003c/p\u003e\u003cp\u003e\n\tSpeaking about memory, TH05 already extends the bullet structure by 6 bytes\n\tfor the \"exact linear movement\" type exclusive to that game. This type is\n\tparticularly interesting for all the prospective PC-98 game developers out\n\tthere, as it nicely points out the precision limits of Q12.4 subpixels.\n\t\u003cbr\u003e\n\tRegular bullet movement works by adding a Q12.4 velocity to a Q12.4 position\n\tevery frame, with the velocity typically being calculated only once on spawn\n\ttime from an 8-bit angle and a Q12.4 speed. Quantization errors from this\n\tinitial calculation can quickly compound over all the frames a bullet spends\n\tmoving across the playfield. If a bullet is only supposed to move on a\n\tstraight line though, there is a more precise way of calculating its\n\tposition: By storing the origin point, movement angle, and total distance\n\ttraveled, you can perform a full polar→Cartesian transformation every frame.\n\tOut of the 10 danmaku patterns in TH05 that use this motion type, the\n\tdifference to regular bullet movement can be best seen in Louise's final\n\tpattern:\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-03-05-TH05-Louise-final-original.webp?eff9f6de\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"565\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-03-05-TH05-Louise-final-original.avi?beb9a0b3\"\u003e\u003csource src=\"/blog/static/video/av1/2022-03-05-TH05-Louise-final-original.webm?02d44e9c\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-03-05-TH05-Louise-final-original.webm?18fd2d67\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-03-05-TH05-Louise-final-original.webm?96f3e43f\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-03-05-TH05-Louise-final-original.avi?beb9a0b3\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eLouise's final pattern in its original form, demonstrating\n\t\texact linear bullet movement. Note how each bullet spawns slightly\n\t\tbehind the delay cloud: ZUN simply forgot to shift the fixed origin\n\t\tpoint along with it.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure style=\"width: 384px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-03-05-TH05-Louise-final-regular-movement.webp?6274e227\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"565\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-03-05-TH05-Louise-final-regular-movement.avi?a6ea292b\"\u003e\u003csource src=\"/blog/static/video/av1/2022-03-05-TH05-Louise-final-regular-movement.webm?10b86817\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-03-05-TH05-Louise-final-regular-movement.webm?f5ba97d3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-03-05-TH05-Louise-final-regular-movement.webm?d4f0e7db\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2022-03-05-TH05-Louise-final-regular-movement.avi?a6ea292b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eThe same pattern with standard bullet movement, corrupting\n\t\tits intended appearance. No delay cloud-related oversights here though,\n\t\tat least.\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tNot far away from the regular bullet code, we've also got the movement\n\tfunction for the infamous curve\u0026nbsp;/ \"cheeto\" bullets. I would have almost\n\tcalled them \"cheetos\" in the code as well, which surely fits more nicely\n\tinto 8.3 filenames than \"curve bullets\" does, but eh, trademarks…\n\u003c/p\u003e\u003cp\u003e\n\tAs for hitboxes, we got a 16×16 one on the head node, and a 12×12 one on the\n\t16 trail nodes. The latter simply store the position of the head node during\n\tthe last 16 frames, Snake style. But what you're all here for is probably\n\tthe turning and homing algorithm, right? Boiled down to its essence, it\n\tworks like this:\n\u003c/p\u003e\u003cpre\u003e// [head] points to the controlled \"head\" part of a curve bullet entity.\n// Angles are stored with 8 bits representing a full circle, providing free\n// normalization on arithmetic overflow.\n// The directions are ordered as you would expect:\n// • 0x00: right\t(sin(0x00) =  0, cos(0x00) = +1)\n// • 0x40: down \t(sin(0x40) = +1, cos(0x40) =  0)\n// • 0x80: left \t(sin(0x80) =  0, cos(0x80) = -1)\n// • 0xC0: up   \t(sin(0xC0) = -1, cos(0xC0) =  0)\nuint8_t angle_delta = (head-\u003eangle - player_angle_from(\n\thead-\u003epos.cur.x, head-\u003epos.cur.y\n));\n\n// Stop turning if the player is 1/128ths of a circle away from this bullet\nconst uint8_t SNAP = 0x02;\n\n// Else, turn either clockwise or counterclockwise by 1/256th of a circle,\n// depending on what would reach the player the fastest.\nif((angle_delta \u003e SNAP) \u0026\u0026 (angle_delta \u0026lt; static_cast\u0026lt;uint8_t\u0026gt;(-SNAP))) {\n\tangle_delta = (angle_delta \u003e= 0x80) ? -0x01 : +0x01;\n}\nhead_p-\u003eangle -= angle_delta;\n\u003c/pre\u003e\u003cp\u003e\n\t5 lines of code, and not all too difficult to follow once you are familiar\n\twith 8-bit angles… unlike what ZUN \u003ci\u003eactually\u003c/i\u003e wrote. Which is 26 lines,\n\tand includes an unused \"friction\" variable that is never set to any value\n\tthat makes a difference in the formula. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e uth05win\n\tcorrectly saw through that all and simplified this code to something\n\tequivalent to my explanation. Redoing that work certainly wasted a bit of my\n\ttime, and means that I now definitely need to spend another push on RE'ing\n\tall the shared boss functions before I can start with Shinki.\n\u003c/p\u003e\u003cp\u003e\n\tSo while a curve bullet's \u003ci\u003espeed\u003c/i\u003e does get faster over time, its\n\tangular velocity is always limited to \u003csup\u003e1\u003c/sup\u003e/\u003csub\u003e256\u003c/sub\u003eth of a\n\tcircle per frame. This reveals the optimal strategy for dodging them:\n\tMaximize this delta angle by staying as close to 180° away from their\n\tcurrent direction as possible, and let their acceleration do the rest.\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-03-05-TH05-Curve-bullet-dodge-strategy.webp?5bf02e7c\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"56.423132\" data-frame-count=\"979\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2022-03-05-TH05-Curve-bullet-dodge-strategy.avi?a7384da9\"\u003e\u003csource src=\"/blog/static/video/av1/2022-03-05-TH05-Curve-bullet-dodge-strategy.webm?3668f495\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-03-05-TH05-Curve-bullet-dodge-strategy.webm?64e25c78\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-03-05-TH05-Curve-bullet-dodge-strategy.webm?aed06994\" type=\"video/webm\"\u003eVideo demonstrating the optimal strategy for dodging TH05's curved bullets during the Shinki fight. \u003ca href=\"/blog/static/video/zmbv/2022-03-05-TH05-Curve-bullet-dodge-strategy.avi?a7384da9\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAt least that's the theory for dodging a \u003ci\u003esingle\u003c/i\u003e one. As a danmaku\n\tdesigner, you can now of course place other bullets at these technically\n\toptimal places to prevent a curve bullet pattern from being cheesed like\n\tthat. I certainly didn't record the video above in a single take either…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter another bunch of boring entity spawn and update functions, the\n\tplayfield shaking feature turned out as the most notable (and tricky) one to\n\tround out these two pushes. It's actually implemented quite well in how it\n\tsimply \"un-shakes\" the screen by just marking every stage tile to be\n\tredrawn. In the context of all the other tile invalidation that can take\n\tplace during a frame, that's definitely more performant than\n\t\u003ca href=\"/blog/2019-11-18\"\u003e📝 doing another EGC-accelerated \u003ccode\u003ememmove()\u003c/code\u003e\u003c/a\u003e.\n\tDue to these two games being double-buffered via page flipping, this\n\tinvalidation only really \u003ci\u003eneeds\u003c/i\u003e to happen for the frame after the next\n\tone though. The immediately next frame will show the regular, un-shaken\n\tplayfield on the other VRAM page first, \u003ci\u003eexcept\u003c/i\u003e during the multi-frame\n\tshake animation when defeating a midboss, where it will also appear shifted\n\tin a different direction… 😵 Yeah, no wonder why ZUN just always invalidates\n\tall stage tiles for the next two frames after every shaking animation, which\n\tis guaranteed to handle both sporadic single-frame shakes and continuous\n\tones. \u003ci\u003eSo\u003c/i\u003e close to \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e here.\n\u003c/p\u003e\u003cp\u003e\n\tFinally, this delivery was delayed a bit because \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\n\trequested his round-up amount to be limited to the cap in the future. Since\n\tthat makes it kind of hard to explain on a static page how much money he\n\twill exactly provide, I now properly modeled these discounts in the website\n\tcode. The exact round-up amount is now included in both the pre-purchase\n\tbreakdown, as well as the cap bar on the main page.\u003cbr\u003e\n\tWith that in place, the system is now also set up for round-up offers from\n\tother patrons. If you'd also like to support certain goals in this way, with\n\tany amount of money, now's the time for getting in touch with me about that.\n\tKnown contributors only, though! 😛\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The final bunch of shared boring boss functions. Which certainly\n\twill give me a break from all the maintenance and research work, and speed\n\tup delivery progress again… right?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-03-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-02-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-03-05T22:55:32Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-02-18",
      "url": "https://rec98.nmlgc.net/blog/2022-02-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-03-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-01-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-02-18\"\u003e\u003ctime datetime=\"2022-02-18T19:40:52Z\"\u003e2022-02-18 19:40\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0182\"\u003eP0182\u003c/a\u003e\n\t\t\tTH03 RE (Collision bitmap)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/313450f...1e2c7ad\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0183\"\u003eP0183\u003c/a\u003e\n\t\t\tTH03 decompilation (Player collision detection)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1e2c7ad...f9d983e\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e, [Anonymous], Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBeen \u003ca href=\"/blog/2020-02-23\"\u003e📝 a while\u003c/a\u003e since we last looked at any of\n\tTH03's game code! But before that, we need to talk about Y coordinates.\n\u003c/p\u003e\u003cp\u003e\n\tDuring TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e, the PC-98 graphics GDC runs in its\n\tline-doubled 640×200 resolution, which gives the in-game portion its\n\tdistinctive stretched low-res look. This lower resolution is a consequence\n\tof using \u003ca href=\"/blog/2019-11-06\"\u003e📝 Promisence Soft's SPRITE16 driver\u003c/a\u003e:\n\tIts performance simply stems from the fact that it expects sprites to be\n\tstored in the bottom half of VRAM, which allows them to be blitted using the\n\tsame EGC-accelerated VRAM-to-VRAM copies we've seen again and again in all\n\tother games. Reducing the visible resolution also means that the sprites can\n\tbe stored on both VRAM pages, allowing the game to still be double-buffered.\n\tIf you force the graphics chip to run at 640×400, you can see them:\n\u003c/p\u003e\u003cfigure class=\"pixelated fullres\"\u003e\n\t\u003cfigcaption\u003e\n\t\t\n\t\t\n\t\t\u003cspan id=\"2022-02-18-resolution-caption\"\u003eThe full VRAM contents during TH03\u0026#39;s in-game portion, as seen when forcing the system into a 640×400 resolution.\u003c/span\u003e\u003cbr\u003e\n\t\t\u003cbutton id=\"2022-02-18-200-set\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-400').classList.remove('active');\n\t\t\tdocument.getElementById('2022-02-18-200').classList.add('active');\n\t\t\tdocument.getElementById('2022-02-18-200-set').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-400-set').hidden = false;\n\t\t\tdocument.getElementById('2022-02-18-resolution-caption').innerHTML = \u0026#34;The upper half of VRAM during TH03\u0026#39;s in-game portion, at the original line-doubled 640×200 resolution.\u0026#34;;\n\t\t\"\u003e(Switch to line-doubled 640×200)\u003c/button\u003e\n\t\t\u003cbutton id=\"2022-02-18-400-set\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-400').classList.add('active');\n\t\t\tdocument.getElementById('2022-02-18-200').classList.remove('active');\n\t\t\tdocument.getElementById('2022-02-18-400-set').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-200-set').hidden = false;\n\t\t\tdocument.getElementById('2022-02-18-resolution-caption').innerHTML = \u0026#34;The full VRAM contents during TH03\u0026#39;s in-game portion, as seen when forcing the system into a 640×400 resolution.\u0026#34;;\n\t\t\" hidden\u003e(Switch to 640×400)\u003c/button\u003e •\n\t\t\u003cbutton id=\"2022-02-18-tram-hide\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-tram').classList.remove('active');\n\t\t\tdocument.getElementById('2022-02-18-tram-hide').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-tram-show').hidden = false;\n\t\t\"\u003e(Hide text layer)\u003c/button\u003e\n\t\t\u003cbutton id=\"2022-02-18-tram-show\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-tram').classList.add('active');\n\t\t\tdocument.getElementById('2022-02-18-tram-show').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-tram-hide').hidden = false;\n\t\t\" hidden\u003e(Show text layer)\u003c/button\u003e\n\t\u003c/figcaption\u003e\n\t\u003cdiv class=\"multilayer\"\u003e\n\t\t\u003cimg\n\t\t\tid=\"2022-02-18-200\"\n\t\t\tsrc=\"/blog/static/2022-02-18-TH03-200line.png?91ca0fa7\"\n\t\t\talt=\"TH03's VRAM at regular line-doubled 640×200 resolution\"\n\t\t\u003e\u003cimg\n\t\t\tid=\"2022-02-18-400\"\n\t\t\tclass=\"active\"\n\t\t\tsrc=\"/blog/static/2022-02-18-TH03-400line.png?82d39d03\"\n\t\t\talt=\"TH03's VRAM at full 640×400 resolution, including the SPRITE16 sprite area\"\n\t\t\u003e\u003cimg\n\t\t\tid=\"2022-02-18-tram\"\n\t\t\tclass=\"active\"\n\t\t\tsrc=\"/blog/static/2022-02-18-TH03-text-layer.png?d7b7c7b5\"\n\t\t\talt=\"TH03's text layer during an in-game round.\"\n\t\t\u003e\n\t\u003c/div\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNote that the text chip still displays its overlaid contents at 640×400,\n\twhich means that TH03's in-game portion \u003ci\u003etechnically\u003c/i\u003e runs at two\n\tresolutions at the same time.\n\u003c/p\u003e\u003cp\u003e\n\tBut that means that any mention of a Y coordinate is ambiguous: Does it\n\trefer to undoubled VRAM pixels, or on-screen stretched pixels? Especially\n\tpeople who have known about the line doubling for years might almost expect\n\ttechnical blog posts on this game to use undoubled VRAM coordinates. So,\n\tlet's introduce a new formatting convention for both on-screen\n\t640×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e400\u003c/span\u003e and undoubled 640×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e200\u003c/span\u003e coordinates,\n\tand always write out both to minimize the confusion.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, now what's \u003ci\u003ethe thing\u003c/i\u003e gonna be? The enemy structure is highly\n\toverloaded, being used for enemies, fireballs, and explosions with seemingly\n\tdifferent semantics for each. Maybe a bit too much to be figured out in what\n\tshould ideally be a single push, especially with all the functions that\n\twould need to be decompiled? Bullet code would be easier, but not exactly\n\tsingle-push material either. As it turns out though, there's something more\n\tfundamental left to be done first, which both of these subsystems depend on:\n\tcollision detection!\n\u003c/p\u003e\u003cp\u003e\n\tAnd it's implemented exactly how I always naively imagined collision\n\tdetection to be implemented in a fixed-resolution 2D bullet hell game with\n\tsmall hitboxes: By keeping a separate 1bpp bitmap of both playfields in\n\tmemory, drawing in the collidable regions of all entities on every frame,\n\tand then checking whether any pixels at the current location of the player's\n\thitbox are set to 1. It's probably not done in the other games because their\n\tsingle data segment was already too packed for the necessary 17,664 bytes to\n\tstore such a bitmap at pixel resolution, and 282,624 bytes for a bitmap at\n\tQ12.4 subpixel resolution would have been prohibitively expensive in 16-bit\n\tReal Mode DOS anyway. In TH03, on the other hand, this bitmap is doubly\n\tuseful, as the AI also uses it to elegantly learn what's on the playfield.\n\tBy halving the resolution and only tracking tiles of 2×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e2\u003c/span\u003e\u0026nbsp;/ 2×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e1\u003c/span\u003e pixels, TH03 only requires an adequate total\n\tof 6,624 bytes of memory for the collision bitmaps of both playfields.\n\u003c/p\u003e\u003cp\u003e\n\tSo how did the implementation not earn the \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e tag this time? Because the code for drawing into these bitmaps is undecompilable hand-written x86 assembly. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e And not just your usual ASM that was basically compiled from C and then edited to maybe optimize register allocation and maybe replace a bunch of local variables with self-modifying code, oh no. This code is full of overly clever bit twiddling, abusing the fact that the 16-bit \u003ccode\u003eAX\u003c/code\u003e,\n\t\u003ccode\u003eBX\u003c/code\u003e, \u003ccode\u003eCX\u003c/code\u003e, and \u003ccode\u003eDX\u003c/code\u003e registers can also be\n\taccessed as two 8-bit registers, calculations that change the semantic\n\tmeaning behind the value of a register, or just straight-up reassignments of\n\tdifferent values to the same small set of registers. Sure, in some way it is\n\timpressive, and it all \u003ci\u003edoes\u003c/i\u003e work and correctly covers every edge\n\tcase, but \u003ci\u003ecome on\u003c/i\u003e. This could have all been a lot more readable in\n\texchange for just a few CPU cycles.\n\u003c/p\u003e\u003cp\u003e\n\tWhat's most interesting though are the actual shapes that these functions\n\tdraw into the collision bitmap. On the surface, we have:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003evertical slopes at any angle across the whole playfield; exclusively\n\tused for Chiyuri's diagonal laser EX attack\u003c/li\u003e\n\t\u003cli\u003estraight vertical lines, with a width of 1 tile; exclusively used for\n\tthe 2×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e2\u003c/span\u003e\u0026nbsp;/ 2×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e1\u003c/span\u003e hitboxes of bullets\u003c/li\u003e\n\t\u003cli\u003erectangles at arbitrary sizes\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tBut only 2) actually draws a full solid line. 1) and 3) are only ever drawn\n\tas horizontal \u003ci\u003estripes\u003c/i\u003e, with a hardcoded distance of 2 vertical tiles\n\tbetween every stripe of a slope, and 4 vertical tiles between every stripe\n\tof a rectangle. That's 66-75% of each rectangular entity's intended hitbox\n\tnot actually taking part in collision detection. Now, if player hitboxes\n\twere ≤ \u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e6\u003c/span\u003e\u0026nbsp;/ \u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e3\u003c/span\u003e pixels, we'd have one\n\tpossible explanation of how the AI can \"cheat\", because it could just\n\tprecisely move through those blank regions at TAS speeds. So, let's make\n\tthis two pushes after all and tell the complete story, since this is one of\n\tthe more interesting aspects to still be documented in this game.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd the code only gets worse. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e While the player\n\tcollision detection function \u003ci\u003eis\u003c/i\u003e decompilable, it might as well not\n\thave been, because it's just more of the same \"optimized\", hard-to-follow\n\tassembly. With the four splittable 16-bit registers having a total of 20\n\tdifferent meanings in this function, I would have almost \u003ci\u003epreferred\u003c/i\u003e\n\tself-modifying code…\n\u003c/p\u003e\u003cp\u003e\n\tIn fact, it was so bad that it prompted some maintenance work on my inline\n\tassembly coding standards as a whole. Turns out that the \u003ccode\u003e_asm\u003c/code\u003e\n\tkeyword is not only still supported in modern Visual Studio compilers, but\n\talso in Clang with the \u003ccode\u003e-fms-extensions\u003c/code\u003e flag, and compiles fine\n\tthere even for 64-bit targets. While that might sound like amazing news at\n\tfirst \u003ci\u003e(\"awesome, no need to rewrite this stuff for my x86_64 Linux\n\tport!\")\u003c/i\u003e, you quickly realize that almost all inline assembly in this\n\tcodebase assumes either PC-98 hardware, segmented 16-bit memory addressing,\n\tor is a temporary hack that will be removed with further RE progress.\u003cbr\u003e\n\tThat's mainly because most of the raw arithmetic code uses Turbo C++'s\n\tregister pseudovariables where possible. While they certainly have their\n\tdrawbacks, being a non-standard extension that's not supported in other\n\tx86-targeting C compilers, their advantages are quite significant: They\n\tallow this code to stay in the same language, and provide slightly more\n\timmediate portability to any other architecture, together with\n\t\u003ca href=\"/blog/2021-03-20\"\u003e📝 readability and maintainability improvements that can get quite significant when combined with inlining\u003c/a\u003e:\n\u003c/p\u003e\u003cpre\u003e// This one line compiles to five ASM instructions, which would need to be\n// spelled out in any C compiler that doesn't support register pseudovariables.\n// By adding typed aliases for these registers via `#define`, this code can be\n// both made even more readable, and be prepared for an easier transformation\n// into more portable local variables.\n_ES = (((_AX * 4) + _BX) + SEG_PLANE_B);\n\u003c/pre\u003e\u003cp\u003e\n\tHowever, register pseudovariables might cause potential portability issues\n\tas soon as they are mixed with inline assembly instructions that rely on\n\ttheir state. The lazy way of \"supporting pseudo-registers\" in other\n\tcompilers would involve declaring the full set as global variables, which\n\twould immediately break every one of those instances:\n\u003c/p\u003e\u003cpre\u003e_DI = 0;\n_AX = 0xFFFF;\n\n// Special x86 instruction doing the equivalent of\n//\n// \t*reinterpret_cast\u0026lt;uint16_t far *\u0026gt;(MK_FP(_ES, _DI)) = _AX;\n// \t_DI += sizeof(uint16_t);\n//\n// Only generated by Turbo C++ in very specific cases, and therefore only\n// reliably available through inline assembly.\nasm { movsw; }\n\u003c/pre\u003e\u003cp\u003e\n\tWhat's \u003ci\u003ealso\u003c/i\u003e not all too standardized, though, are certain variants of\n\tthe \u003ccode\u003easm\u003c/code\u003e keyword. That's why I've now introduced a distinction\n\tbetween the \u003ccode\u003e_asm\u003c/code\u003e keyword for \"decently sane\" inline assembly,\n\tand the slightly less standard \u003ccode\u003easm\u003c/code\u003e keyword for inline assembly\n\tthat relies on the contents of pseudo-registers, and \u003ci\u003eshould\u003c/i\u003e break on\n\tcompilers that don't support them.\u003cbr\u003e So yeah, have some minor\n\tportability work in exchange for these two pushes not having all that much\n\tin RE'd content.\n\u003c/p\u003e\u003cp\u003e\n\tWith that out of the way and the function deciphered, we can confirm the\n\tplayer hitboxes to be a constant 8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e\u0026nbsp;/\n\t8×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e4\u003c/span\u003e pixels, and prove that the hit stripes are nothing but\n\tan adequate optimization that doesn't affect gameplay in any way.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd what's the obvious thing to immediately do if you have both the\n\tcollision bitmap and the player hitbox? Writing a \"real hitbox\" mod, of\n\tcourse:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eReorder the calls to rendering functions so that player and shot sprites\n\tare rendered after bullets\u003c/li\u003e\n\t\u003cli\u003eBlank out all player sprite pixels outside an\n\t8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e\u0026nbsp;/ 8×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e4\u003c/span\u003e box around the center\n\tpoint\u003c/li\u003e\n\t\u003cli\u003eAfter the bullet rendering function, turn on the GRCG in RMW mode and\n\tset the tile register set to the background color\u003c/li\u003e\n\t\u003cli\u003eStretch the negated contents of collision bitmap onto each playfield,\n\tleaving only collidable pixels untouched\u003c/li\u003e\n\t\u003cli\u003eDo the same with the actual, non-negated contents and a white color, for\n\textra contrast against the background. This also makes sure to show any\n\tcollidable areas whose sprite pixels are transparent, such as with the moon\n\tenemy. (Yeah, how unfair.) Doing that also loses a lot of information about\n\tthe playfield, such as enemy HP indicated by their color, but what can you\n\tdo:\u003c/li\u003e\n\u003c/ol\u003e\u003cfigure class=\"pixelated fullres\"\u003e\n\t\u003cdiv class=\"multilayer\"\u003e\n\t\t\u003cimg\n\t\t\tid=\"2022-02-18-action\"\n\t\t\tclass=\"active\"\n\t\t\tsrc=\"/blog/static/2022-02-18-TH03-action.png?f554a768\"\n\t\t\talt=\"A decently busy TH03 in-game frame.\"\n\t\t\u003e\u003cimg\n\t\t\tid=\"2022-02-18-collmap\"\n\t\t\tsrc=\"/blog/static/2022-02-18-TH03-collmap.png?73b572ed\"\n\t\t\talt=\"The underlying content of the collision bitmap, showing off all three different shapes together with the player hitboxes.\"\n\t\t\u003e\n\t\u003c/div\u003e\n\t\u003cfigcaption\u003e\n\t\tA decently busy TH03 in-game frame and its underlying collision bitmap,\n\t\tshowing off all three different collision shapes together with the\n\t\tplayer hitboxes.\u003cbr\u003e\n\t\t\u003cbutton id=\"2022-02-18-collmap-set\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-action').classList.remove('active');\n\t\t\tdocument.getElementById('2022-02-18-collmap').classList.add('active');\n\t\t\tdocument.getElementById('2022-02-18-collmap-set').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-action-set').hidden = false;\n\t\t\"\u003e(Show collision bitmap contents and player hitboxes)\u003c/button\u003e\n\t\t\u003cbutton id=\"2022-02-18-action-set\" onclick=\"\n\t\t\tdocument.getElementById('2022-02-18-collmap').classList.remove('active');\n\t\t\tdocument.getElementById('2022-02-18-action').classList.add('active');\n\t\t\tdocument.getElementById('2022-02-18-action-set').hidden = true;\n\t\t\tdocument.getElementById('2022-02-18-collmap-set').hidden = false;\n\t\t\" hidden\u003e(Hide collision bitmap contents and player hitboxes)\u003c/button\u003e\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\u003ca class=\"download\" href=\"/blog/static/2022-02-18-TH03-real-hitbox.zip?caa0f8c0\" data-kb=\"65.9\"\u003e2022-02-18-TH03-real-hitbox.zip \u003c/a\u003e\n\tThe secret for writing such mods before having reached a sufficient level of\n\tposition independence? Put your new code segment into \u003ccode\u003eDGROUP\u003c/code\u003e,\n\tpast the end of the uninitialized data section. That's why this modded\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e is a lot larger than you would expect from the raw amount of new code: The file now actually needs to \u003ci\u003estore\u003c/i\u003e all these\n\tuninitialized 0 bytes between the end of the data segment and the first\n\tinstruction of the mod code – normally, this number is simply a part of the\n\tMZ EXE header, and doesn't need to be redundantly stored on disk. Check the\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/tree/th03_real_hitbox\"\u003e\u003ccode\u003eth03_real_hitbox\u003c/code\u003e\u003c/a\u003e\n\tbranch for the code.\n\u003c/p\u003e\u003cp\u003e\n\tAnd now we know why so many \"real hitbox\" mods for the Windows Touhou games\n\tare inaccurate: The games would simply be unplayable otherwise – or can\n\t\u003ci\u003eyou\u003c/i\u003e dodge rapidly moving 2×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e2\u003c/span\u003e\u0026nbsp;/\n\t2×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e1\u003c/span\u003e blocks as an 8×\u003cspan class=\"y screen\" title=\"Y coordinate in on-screen 640×400 space\"\u003e8\u003c/span\u003e\u0026nbsp;/\n\t8×\u003cspan class=\"y vram200\" title=\"Y coordinate in 640×200 VRAM space\"\u003e4\u003c/span\u003e rectangle that is smaller than your shot sprites,\n\tespecially without focused movement? I can't. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tMaybe it will feel more playable after making explosions visible, but that\n\twould need more RE groundwork first.\u003cbr\u003e\n\tIt's also interesting how adding two full GRCG-accelerated redraws of both\n\tplayfields per frame doesn't significantly drop the game's frame rate – so\n\twhy did the drawing functions have to be micro-optimized again? It\n\t\u003ci\u003ewould\u003c/i\u003e be possible in one pass by using the GRCG's TDW mode, which\n\tshould theoretically be 8× faster, but I have to stop \u003ci\u003esomewhere\u003c/i\u003e.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The final missing piece of TH04's and TH05's\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/bullet\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/a\u003e\u003c/span\u003e-moving code, which will include a certain other\n\ttype of projectile as well.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-03-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2022-01-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-02-18T19:40:52Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2022-01-31",
      "url": "https://rec98.nmlgc.net/blog/2022-01-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-02-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2022-01-31\"\u003e\u003ctime datetime=\"2022-01-31T09:17:43Z\"\u003e2022-01-31 09:17\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0174\"\u003eP0174\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 2/9: Preparation + birds)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/27f901c...a0fe812\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0175\"\u003eP0175\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 3/9: Shield/wand/dress animation + patterns 1-3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/a0fe812...40ac9a7\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0176\"\u003eP0176\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 4/9: Background transition animation + vertical 2×2 particles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/40ac9a7...c5dc45b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0177\"\u003eP0177\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 5/9: Patterns 4-9 + wavy 2×2 particles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c5dc45b...5f0cabc\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0178\"\u003eP0178\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 6/9: Patterns 10-11)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5f0cabc...60621f8\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0179\"\u003eP0179\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 7/9: Patterns 12-13 + horizontal 2×2 particles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/60621f8...9e5b344\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0180\"\u003eP0180\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 8/9: Patterns 14-16)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9e5b344...091f19f\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0181\"\u003eP0181\u003c/a\u003e\n\t\t\tTH01 decompilation (Sariel, part 9/9: Main function)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/091f19f...313450f\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sariel\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 魔界/Makai route.\"\u003esariel\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/good-code\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tHere we go, TH01 Sariel! This is the single biggest boss fight in all of\n\tPC-98 Touhou: If we include all custom effect code we previously decompiled,\n\tit amounts to a total of \u003cspan class=\"hovertext\" title=\"And that's just raw instruction numbers. Those might be not that representative as they include code shared between the executables, with single C++ translation units being counted two or three times.\"\u003e10.31% of all code in TH01 (and 3.14%\n\toverall)\u003c/span\u003e. These 8 pushes cover the final 8.10% (or 2.47% overall),\n\tand are likely to be the single biggest delivery this project will ever see.\n\tConsidering that I only managed to decompile 6.00% across all games in 2021,\n\t2022 is already off to a much better start!\n\u003c/p\u003e\u003cp\u003e\n\tSo, how can Sariel's code be that large? Well, we've got:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e16 danmaku patterns; including the one snowflake detonating into a giant\n\t94×32 hitbox\u003c/li\u003e\n\t\u003cli\u003eGratuitous usage of floating-point variables, bloating the binary thanks\n\tto Turbo C++ 4.0J's particularly horrid code generation\u003c/li\u003e\n\t\u003cli\u003eThe hatching birds that shoot pellets\u003c/li\u003e\n\t\u003cli\u003e3 separate particle systems, sharing the general idea, overall code\n\tstructure, and blitting algorithm, but differing in every little detail\u003c/li\u003e\n\t\u003cli\u003eThe \"gust of wind\" background transition animation\u003c/li\u003e\n\t\u003cli\u003e5 sets of custom monochrome sprite animations, loaded from\n\t\u003ccode\u003eBOSS6GR?.GRC\u003c/code\u003e\u003c/li\u003e\n\t\u003cli\u003eA further 3 hardcoded monochrome 8×8 sprites for the \"swaying leaves\"\n\tpattern during the second form\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn total, it's just under 3,000 lines of C++ code, containing a total of 8\n\tdefinite ZUN bugs, 3 of them being subpixel/pixel confusions. That might not\n\tlook all \u003ci\u003etoo\u003c/i\u003e bad if you compare it to the\n\t\u003ca href=\"/blog/2021-10-20\"\u003e📝 player control function's 8 bugs in 900 lines of code\u003c/a\u003e,\n\tbut given that Konngara had \u003cs\u003e0\u003c/s\u003e… (\u003cstrong\u003eEdit (2022-07-17):\u003c/strong\u003e\n\tKonngara contains two bugs after all: A\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 possible heap corruption in test or debug mode\u003c/a\u003e,\n\tand the infamous\n\t\u003ca href=\"/blog/2022-07-17\"\u003e📝 temporary green discoloration\u003c/a\u003e.)\n\tAnd no, the code doesn't make it obvious whether ZUN coded Konngara or\n\tSariel first; there's just as much evidence for either.\n\u003c/p\u003e\u003cp\u003e\n\tSome terminology before we start: Sariel's first \u003ci\u003eform\u003c/i\u003e is separated\n\tinto four \u003ci\u003ephases\u003c/i\u003e, indicated by different background images, that\n\tcycle until Sariel's HP reach 0 and the second, single-phase \u003ci\u003eform\u003c/i\u003e\n\tstarts. The danmaku \u003ci\u003epatterns\u003c/i\u003e within each phase are also on a cycle,\n\tand the game picks a random but limited number of patterns per phase before\n\ttransitioning to the next one. The fight always starts at pattern 1 of phase\n\t1 (the random purple lasers), and each new phase also starts at its\n\trespective first pattern.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSariel's bugs already start at the graphics asset level, before any code\n\tgets to run. Some of the patterns include a wand raise animation, which is\n\tstored in \u003ccode\u003eBOSS6_2.BOS\u003c/code\u003e:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\n\t\u003ca href=\"/blog/static/2022-01-31-TH01-BOSS6_2.png?3db35e64\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-01-31-TH01-BOSS6_2.png?3db35e64\" alt=\"TH01 BOSS6_2.BOS\" style=\"height: 192px\"\u003e\u003c/a\u003e\n\t\u003cfigcaption\u003eUmm… OK? The same sprite twice, just with slightly different\n\tcolors? So how is the wand lowered again?\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe \"lowered wand\" sprite is missing in this file simply because it's\n\tcaptured from the regular background image in VRAM, at the beginning of the\n\tfight and after every background transition. What I previously thought to be\n\t\u003ca href=\"/blog/2021-11-08\"\u003e📝 background storage code\u003c/a\u003e has therefore a\n\tdifferent meaning in Sariel's case. Since this captured sprite is fully\n\topaque, it will reset the entire 128×128 wand area… wait, 128×128, rather\n\tthan 96×96? Yup, this lowered sprite is larger than necessary, wasting 1,967\n\tbytes of conventional memory.\u003cbr\u003e That still doesn't quite explain the\n\tsecond sprite in \u003ccode\u003eBOSS6_2.BOS\u003c/code\u003e though. Turns out that the black\n\tpart is indeed \u003ci\u003emeant to unblit the purple reflection (?) in the first\n\tsprite\u003c/i\u003e. But… that's not how you would correctly unblit that?\n\u003c/p\u003e\u003cfigure class=\"side_by_side pixelated\"\u003e\n\t\u003ca href=\"/blog/static/2022-01-31-TH01-Sariel-wand-raise-1.png?11cc70d7\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-01-31-TH01-Sariel-wand-raise-1.png?11cc70d7\"\n\t\talt=\"VRAM after blitting the first sprite of TH01's BOSS6_2.BOS\"\n\t\tstyle=\"height: 224px;\"\u003e\u003c/a\u003e\n\t\u003ca href=\"/blog/static/2022-01-31-TH01-Sariel-wand-raise-2.png?8b062500\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2022-01-31-TH01-Sariel-wand-raise-2.png?8b062500\"\n\t\talt=\"VRAM after blitting the second sprite of TH01's BOSS6_2.BOS\"\n\t\tstyle=\"height: 224px;\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe first sprite already eats up part of the red HUD line, and the second\n\tone additionally fails to recover the seal pixels underneath, leaving a nice\n\tlittle black hole and some stray purple pixels until the next background\n\ttransition. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Quite ironic given that both\n\tsprites do include the right part of the seal, which isn't even part of the\n\tanimation.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tJust like Konngara, Sariel continues the approach of using a single function\n\tper danmaku pattern or custom entity. While I appreciate that this allows\n\tall pattern- and entity-specific state to be scoped locally to that one\n\tfunction, it quickly gets ugly as soon as such a function has to do more than one thing.\u003cbr\u003e\n\tThe \"bird function\" is particularly awful here: It's just one \u003ccode\u003eif(…)\n\t{…} else\u0026nbsp;if(…) {…} else\u0026nbsp;if(…) {…}\u003c/code\u003e chain with different\n\tbranches for the subfunction parameter, with zero shared code between any of\n\tthese branches. It also uses 64-bit floating-point \u003ccode\u003edouble\u003c/code\u003e as\n\tits subpixel type… and since it also takes four of those as parameters\n\t(y'know, just in case the \"spawn new bird\" subfunction is called), every\n\tcall site has to also push four \u003ccode\u003edouble\u003c/code\u003e values onto the stack.\n\tThanks to Turbo C++ even using the FPU for \u003ci\u003epushing a 0.0 constant\u003c/i\u003e, we\n\thave already reached maximum floating-point decadence before even having\n\tseen a single danmaku pattern. Why decadence? Every possible spawn position\n\tand velocity in both bird patterns just uses pixel resolution, with no\n\tfractional component in sight. And there goes another 720 bytes of\n\tconventional memory.\n\u003c/p\u003e\u003cp\u003e\n\tSpeaking about bird patterns, the red-bird one is where we find the first\n\tcode-level ZUN bug: The spawn cross circle sprite suddenly disappears after\n\tit finished spawning all the bird eggs. How can we tell it's a bug? Because\n\tthere \u003ci\u003eis\u003c/i\u003e code to smoothly fly this sprite off the playfield, that\n\tcode just suddenly forgets that the sprite's position is stored in Q12.4\n\tsubpixels, and treats it as raw screen pixels instead.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e As a result, the well-intentioned 640×400\n\tscreen-space clipping rectangle effectively shrinks to 38×23 pixels in the\n\ttop-left corner of the screen. Which the sprite is always outside of, and\n\tthus never rendered again.\u003cbr\u003e\n\tThe intended animation is easily restored though:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-01-31-TH01-Sariel-pattern-3-original.webp?85fb19ad\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"399\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-pattern-3-original.avi?dbf8b0f1\"\u003e\u003csource src=\"/blog/static/video/av1/2022-01-31-TH01-Sariel-pattern-3-original.webm?c29398dd\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-01-31-TH01-Sariel-pattern-3-original.webm?1687f857\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-01-31-TH01-Sariel-pattern-3-original.webm?4fd8683c\" type=\"video/webm\"\u003eVideo of TH01 Sariel's \"birds on ellipse arc\" pattern in its original version. \u003ca href=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-pattern-3-original.avi?dbf8b0f1\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-01-31-TH01-Sariel-pattern-3-fixed.webp?6ff306b2\" preload=\"none\" controls data-title=\"Fixed version\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"399\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-pattern-3-fixed.avi?5568a109\"\u003e\u003csource src=\"/blog/static/video/av1/2022-01-31-TH01-Sariel-pattern-3-fixed.webm?8d25e2b1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-01-31-TH01-Sariel-pattern-3-fixed.webm?cadf73eb\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-01-31-TH01-Sariel-pattern-3-fixed.webm?89bf87a6\" type=\"video/webm\"\u003eVideo of TH01 Sariel's \"birds on ellipse arc\" pattern with fixed spawn cross cirle movement and bird hatch animations. \u003ca href=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-pattern-3-fixed.avi?5568a109\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tSariel's third pattern, and the first to spawn birds, in its original\n\t\tand dequirked versions. Note that I somewhat fixed the bird hatch animation\n\t\tas well: ZUN's code never unblits any frame of animation there, and\n\t\tsimply blits every new one on top of the previous one.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAlso, did you know that birds actually have a quite unfair 14×38-pixel\n\thitbox? Not that you'd ever collide with them in any of the patterns…\n\u003c/p\u003e\u003cp\u003e\n\tAnother 3 of the 8 bugs can be found in the symmetric, interlaced spawn rays\n\tused in three of the patterns, and the 32×32 debris \"sprites\" \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhYAAgAPABAAAAAP///yH5BAUKAAEALAAAAABgACAAAAJVjI+py+0Po5y0Wgqu3jHzD4biSJaNZ6bqyrauhL4yEs+2XN/6zvda7gtegEOhEXdMKpdMDvHzNESP06a1V736shOuyKsNf8XkCriM3p3TbNO6DW8XAAA7\"\n\t\tstyle=\"vertical-align: middle;\" alt=\"\"\u003e shown at their endpoint, at\n\tthe edge of the screen. You kinda have to commend ZUN's attention to detail\n\there, and how he wrote a lot of code for those few rapidly animated pixels\n\tthat you most likely \u003cspan class=\"hovertext\" title=\"And your brain probably didn't either, with all the sloppy unblitting everywhere…\"\u003edon't\n\teven notice\u003c/span\u003e, especially with all the other \u003ci\u003ewrong\u003c/i\u003e pixels\n\tresulting from rendering glitches. One of the bugs in the very final pattern\n\tof phase 4 even turns them into the vortex sprites from the second pattern\n\tin phase 1 \u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhYAAgAPABAAAAAP///yH5BAUKAAEALAAAAABgACAAAAK0jI+py+0Pozyggomz3sj6y3VgSE5WqVToCqks5b4yNQeePOK1HbP5/roFgbuTjjgzDpGan8j5VDqkqQ9z2kt8hA1uNAZdLrKM7cO7DRexEevZaL52sOpoO11PrcghN53atpeX4dXCx1Zy2Kc4xtiY6Fh2RhgpQoJ2N4kBiMhBNch5WQkzWPZZFyraNGqnpVYouMrKw1kIizL75YN3qyp6UvqXJkdsGFeMnKy8zNzs/Awd7VwAADs=\"\n\t\tstyle=\"vertical-align: middle;\" alt=\"\"\u003e during the first 5 frames of\n\tthe first time the pattern is active, and I had to single-step the blitting\n\tcalls to verify it.\u003cbr\u003e\n\tIt certainly was annoying how much time I spent making sense of these bugs,\n\tand all weird blitting offsets, for just \u003ci\u003ea few pixels\u003c/i\u003e… Let's look at\n\tsomething more wholesome, shall we?\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo far, we've only seen the PC-98 GRCG being used in RMW (read-modify-write)\n\tmode, which I previously\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 explained in the context of TH01's red-white HP pattern\u003c/a\u003e.\n\tThe second of its three modes, TCR (Tile Compare Read), affects VRAM reads\n\trather than writes, and performs \"color extraction\" across all 4 bitplanes:\n\tInstead of returning raw 1bpp data from one plane, a VRAM read will instead\n\treturn a bitmask, with a 1 bit at every pixel whose full 4-bit color exactly\n\tmatches the color at that offset in the GRCG's tile register, and 0\n\teverywhere else. Sariel uses this mode to make sure that the 2×2 particles\n\tand the wind effect are only blitted on top of \"air color\" pixels, with\n\tother parts of the background behaving like a mask. The algorithm:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eSet the GRCG to TCR mode, and all 8 tile register dots to the air\n\tcolor\u003c/li\u003e\n\t\u003cli\u003eRead N bits from the target VRAM position to obtain an N-bit mask where\n\tall 1 bits indicate air color pixels at the respective position\u003c/li\u003e\n\t\u003cli\u003eAND that mask with the alpha plane of the sprite to be drawn, shifted to\n\tthe correct start bit within the 8-pixel VRAM byte\u003c/li\u003e\n\t\u003cli\u003eSet the GRCG to RMW mode, and all 8 tile register dots to the color that\n\tshould be drawn\u003c/li\u003e\n\t\u003cli\u003eWrite the previously obtained bitmask to the same position in VRAM\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tQuite clever how the extracted colors double as a secondary alpha plane,\n\tmaking for another well-earned \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e tag. The wind effect really doesn't deserve it, though:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eZUN calculates \u003ci\u003eevery\u003c/i\u003e intermediate result inside this function\n\t\u003ci\u003eover\u003c/i\u003e and \u003ci\u003eover\u003c/i\u003e and \u003ci\u003eover\u003c/i\u003e again… Together with some ugly\n\tpointer arithmetic, this function turned into one of the most tedious\n\tdecompilations in a long while.\u003c/li\u003e\n\t\u003cli\u003eThis gradual effect is blitted exclusively to the front page of VRAM,\n\tsince parts of it need to be unblitted to create the illusion of a gust of\n\twind. Then again, anything that moves on top of air-colored background –\n\tmost likely the Orb – will also unblit whatever it covered of the effect…\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs far as I can tell, ZUN didn't use TCR mode anywhere else in PC-98 Touhou.\n\tTune in again later during a TH04 or TH05 push to learn about TDW, the final\n\tGRCG mode!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSpeaking about the 2×2 particle systems, why do we need three of them? Their\n\tonly observable difference lies in the way they move their particles:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eUp or down in a straight line (used in phases 4 and 2,\n\trespectively)\u003c/li\u003e\n\t\u003cli\u003eLeft or right in a straight line (used in the second form)\u003c/li\u003e\n\t\u003cli\u003eLeft and right in a sinusoidal motion (used in phase 3, the \"dark\n\torange\" one)\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tOut of all possible formats ZUN could have used for storing the positions\n\tand velocities of individual particles, he chose a) 64-bit /\n\tdouble-precision floating-point, and b) raw screen pixels. Want to take a\n\tguess at which data type is used for which particle system?\n\u003c/p\u003e\u003cp\u003e\n\tIf you picked \u003ccode\u003edouble\u003c/code\u003e for 1) and 2), and raw screen pixels for\n\t3), you are of course correct! \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e Not that I'm implying\n\tthat it should have been the other way round – screen pixels would have\n\tperfectly fit all three \u003cs\u003esystems\u003c/s\u003e use cases, as all 16-bit coordinates\n\tare extended to 32 bits for trigonometric calculations anyway. That's what,\n\tanother 1.080 bytes of wasted conventional memory? And that's even\n\tcalculated \u003ci\u003ewhile\u003c/i\u003e keeping the current architecture, which allocates\n\tspace for 3×30 particles as part of the game's global data, although only\n\tone of the three particle systems is active at any given time.\n\u003c/p\u003e\u003cp\u003e\n\tThat's it for the first form, time to put on \u003cspan lang=\"ja\"\u003e\"Civilization\n\tof Magic\"\u003c/span\u003e! Or \u003cspan lang=\"ja\"\u003e\"死なばもろとも\"\u003c/span\u003e? Or \u003cspan\n\tlang=\"ja\"\u003e\"Theme of 地獄めくり\"\u003c/span\u003e? Or whatever \u003ccode\u003eSYUGEN\u003c/code\u003e is\n\tsupposed to mean…\n\u003cp\u003e\u003chr\u003e\u003cp\u003e\n\t… and the code of these final patterns comes out roughly as exciting as\n\ttheir in-game impact. With the big exception of the very final \"swaying\n\tleaves\" pattern: After \u003ca href=\"/blog/2019-12-05\"\u003e📝 Q4.4\u003c/a\u003e,\n\t\u003ca href=\"/blog/2020-09-12\"\u003e📝 Q28.4\u003c/a\u003e,\n\t\u003ca href=\"/blog/2020-10-06\"\u003e📝 Q24.8\u003c/a\u003e, and \u003ccode\u003edouble\u003c/code\u003e variables,\n\tthis pattern uses… \u003ci\u003edecimal\u003c/i\u003e subpixels? Like, multiplying the number by\n\t10, and using the decimal one's digit to represent the fractional part?\n\tWell, sure, if you \u003ci\u003ereally\u003c/i\u003e insist on moving the leaves in cleanly\n\trepresented integer multiples of ⅒, which is infamously impossible in IEEE\n\t754. Aside from aesthetic reasons, it only really combines less precision\n\t(10 possible fractions rather than the usual 16) with the inferior\n\tperformance of having to use integer divisions and multiplications rather\n\tthan simple bit shifts. And it's surely not because the leaf sprites needed\n\tan extended integer value range of [﻿-3276,\u0026nbsp;+3276﻿], compared to\n\tQ12.4's [﻿-2047,\u0026nbsp;+2048﻿]: They are clipped to 640×400 screen space\n\tanyway, and are removed as soon as they leave this area.\n\u003c/p\u003e\u003cp\u003e\n\tThis pattern also contains the second bug in the \"subpixel/pixel confusion\n\thiding an entire animation\" category, causing all of\n\t\u003ccode\u003eBOSS6GR4.GRC\u003c/code\u003e to effectively become \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e:\n\u003c/p\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-01-31-TH01-Sariel-leaf-splash-original.webp?e1e9dba6\" preload=\"none\" controls data-title=\"Original version\" loop data-active width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"440\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-leaf-splash-original.avi?787a9ae3\"\u003e\u003csource src=\"/blog/static/video/av1/2022-01-31-TH01-Sariel-leaf-splash-original.webm?f5a93342\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-01-31-TH01-Sariel-leaf-splash-original.webm?06a7242f\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-01-31-TH01-Sariel-leaf-splash-original.webm?3856d721\" type=\"video/webm\"\u003e(Video of the missing animation in TH01 Sariel's \"swaying leaves\" pattern. \u003ca href=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-leaf-splash-original.avi?787a9ae3\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003cvideo poster=\"/blog/static/video/poster/2022-01-31-TH01-Sariel-leaf-splash-fixed.webp?e1e9dba6\" preload=\"none\" controls data-title=\"Fixed version\" loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"440\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-leaf-splash-fixed.avi?c74c73a4\"\u003e\u003csource src=\"/blog/static/video/av1/2022-01-31-TH01-Sariel-leaf-splash-fixed.webm?28a899a2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2022-01-31-TH01-Sariel-leaf-splash-fixed.webm?260b7216\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2022-01-31-TH01-Sariel-leaf-splash-fixed.webm?2dce9a5a\" type=\"video/webm\"\u003eVideo of the restored animation in TH01 Sariel's \"swaying leaves\" pattern. \u003ca href=\"/blog/static/video/zmbv/2022-01-31-TH01-Sariel-leaf-splash-fixed.avi?c74c73a4\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\u003cfigcaption\u003e\n\t\tThe \"swaying leaves\" pattern. ZUN intended a splash animation to be\n\t\tshown once each leaf \"spark\" reaches the top of the playfield, which is\n\t\tnever displayed in the original game.\n\t\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tAt least their hitboxes are what you would expect, exactly covering the\n\t30×30 pixels of Reimu's sprite. Both animation fixes are available on the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/th01_sariel_fixes\"\u003e\u003ccode\u003eth01_sariel_fixes\u003c/code\u003e\u003c/a\u003e\n\tbranch.\n\u003c/p\u003e\u003cp\u003e\n\tAfter all that, Sariel's main function turned out fairly unspectacular, just\n\tputting everything together and adding some shake, transition, and color\n\tpulse effects with a bunch of unnecessary hardware palette changes. There is\n\tone reference to a missing \u003ccode\u003eBOSS6.GRP\u003c/code\u003e file during the\n\tfirst→second form transition, suggesting that Sariel originally had a\n\tseparate \"first form defeat\" graphic, before it was replaced with just the\n\tshaking effect in the final game.\u003cbr\u003e\n\tSpeaking about the transition code, it is kind of funny how the… um,\n\t\u003ci\u003eimperative\u003c/i\u003e and \u003ci\u003econcrete\u003c/i\u003e nature of TH01 leads to these 2×24\n\tlines of straight-line code. They kind of look like ZUN rattling off a\n\tlaundry list of subsystems and raw variables to be reinitialized, making\n\tdamn sure to not forget anything.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhew! Second PC-98 Touhou boss completely decompiled, 29 to go, and they'll\n\tonly get easier from here! 🎉 The next one in line, Elis, is somewhere\n\tbetween Konngara and Sariel as far as x86 instruction count is concerned, so\n\tthat'll need to wait for some additional funding. Next up, therefore:\n\tLooking at \u003ci\u003ea thing\u003c/i\u003e in TH03's main game code – really, I have little\n\tidea what it will be!\n\u003c/p\u003e\u003cp\u003e\n\tNow that the store is open again, also check out the\n\t\u003ca href=\"/blog/2021-05-13\"\u003e📝 updated RE progress overview\u003c/a\u003e I've posted\n\ttogether with this one. In addition to more RE, you can now also directly\n\torder a variety of mods; all of these are further explained in the order\n\tform itself.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-02-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2022-01-31T09:17:43Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-12-27",
      "url": "https://rec98.nmlgc.net/blog/2021-12-27",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-01-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-12-27\"\u003e\u003ctime datetime=\"2021-12-27T18:19:52Z\"\u003e2021-12-27 18:19\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0172\"\u003eP0172\u003c/a\u003e\n\t\t\tTH03 decompilation (YUME.NEM)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/49e6789...2d5491e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0173\"\u003eP0173\u003c/a\u003e\n\t\t\tTH03 decompilation (High score menu, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/2d5491e...27f901c\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTH03 finally passed 20% RE, and the newly decompiled code contains no\n\tserious ZUN bugs! What a nice way to end the year.\n\u003c/p\u003e\u003cp\u003e\n\tThere's only a single unlockable feature in TH03: Chiyuri and Yumemi as\n\tplayable characters, unlocked after a 1CC on any difficulty. Just like the\n\tExtra Stages in TH04 and TH05, \u003ccode\u003eYUME.NEM\u003c/code\u003e contains a single\n\tdesignated variable for this unlocked feature, making it trivial to craft a\n\tfully unlocked score file without recording any high scores that others\n\twould have to compete against. So, we can now put together a complete set\n\tfor all PC-98 Touhou games: \u003ca class=\"download\" href=\"/blog/static/2021-12-27-Fully-unlocked-clean-score-files.zip?b3104e0d\" data-kb=\"5.4\"\u003e2021-12-27-Fully-unlocked-clean-score-files.zip \u003c/a\u003e\n\tIt would have been cool to set the randomly generated encryption keys in\n\tthese files to a fixed value so that they cancel out and end up not actually\n\tencrypting the file. Too bad that TH03 also started feeding each encrypted\n\tbyte back into its stream cipher, which makes this impossible.\n\u003c/p\u003e\u003cp\u003e\n\tThe main loading and saving code turned out to be the second-cleanest\n\timplementation of a score file format in PC-98 Touhou, just behind TH02.\n\tOnly two of the \u003ccode\u003eYUME.NEM\u003c/code\u003e functions come with nonsensical\n\tdifferences between \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eMAINL.EXE\u003c/code\u003e, rather\n\tthan \u003ca href=\"/blog/2020-03-22\"\u003e📝 all of them, as in TH01\u003c/a\u003e or\n\t\u003ca href=\"/blog/2019-12-28\"\u003e📝 too many of them, as in TH04 and TH05\u003c/a\u003e. As\n\tfor the rest of the per-difficulty structure though… well, it quickly\n\tbecomes clear why this was the final score file format to be RE'd. The name,\n\tscore, and stage fields are directly stored in terms of the internal\n\t\u003ccode\u003eREGI*.BFT\u003c/code\u003e sprite IDs used on the high score screen. TH03 also\n\tstores 10 score digits for each place rather than the 9 possible ones, keeps\n\tany leading 0 digits, and stores the letters of entered names in reverse\n\torder… yeah, let's decompile the high score screen as well, for a full\n\tunderstanding of why ZUN might have done all that. (Answer: For no reason at\n\tall. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd wow, what a breath of fresh air. It's surely not\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e: The overlapping shadows resulting from using\n\ta 24-pixel letterspacing with 32-pixel glyphs in the name column led ZUN to\n\tdo quite a lot of unnecessary and slightly confusing rendering work when\n\tmoving the cursor back and forth, and he even forgot about the EGC there.\n\tBut it's nowhere close to the level of jank we saw in\n\t\u003ca href=\"/blog/2020-05-25\"\u003e📝 TH01's high score menu\u003c/a\u003e last year. Good to\n\tsee that ZUN had learned a thing or two by his third game – especially when\n\tit comes to storing the character map cursor in terms of a character ID,\n\tand improving the layout of the character map:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2021-12-27-TH03-Alphabet.png?fbebdff9\"\u003e\u003cimg src=\"/blog/static/2021-12-27-TH03-Alphabet.png?fbebdff9\" alt=\"The alphabet available for TH03 high score names.\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThat's almost a nicely regular grid there. With the question mark and the\n\tdouble-wide \u003ci\u003eSP\u003c/i\u003e, \u003ci\u003eBS\u003c/i\u003e, and \u003ci\u003eEND\u003c/i\u003e options, the cursor\n\tmovement code only comes with a reasonable two exceptions, which are easily\n\thandled. And while I didn't get this screen \u003ci\u003ecompletely\u003c/i\u003e decompiled,\n\tone additional push was enough to cover all important code there.\n\u003c/p\u003e\u003cp\u003e\n\tThe only potential glitch on this screen is a result of ZUN's continued use\n\tof \u003ca href=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\"\u003ebinary-coded\n\tdecimal\u003c/a\u003e digits without any bounds check or cap. Like the in-game HUD\n\tscore display in TH04 and TH05, TH03's high score screen simply uses the\n\tnext glyph in the character set for the most significant digit of any score\n\tabove 1,000,000,000 points – in this case, the period. Still, it only\n\t\u003ci\u003ereally\u003c/i\u003e gets bad at 8,000,000,000 points: Once the glyphs are\n\texhausted, the blitting function ends up accessing garbage data and filling\n\tthe entire screen with garbage pixels. For comparison though, \u003ca\n\thref=\"https://www.youtube.com/watch?v=LeoY6MEuDdA\"\u003ethe current world record\n\tis 133,650,710 points\u003c/a\u003e, so good luck getting 8 billion in the first\n\tplace.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Starting 2022 with the long-awaited decompilation of TH01's Sariel\n\tfight! Due to the \u003ca href=\"/blog/2021-12-01\"\u003e📝 recent price increase\u003c/a\u003e,\n\twe now got a \u003cscript\u003eformatCurrency(3000)\u003c/script\u003e window in the cap that\n\tis going to remain open until tomorrow, providing an early opportunity to\n\tset a new priority after Sariel is done.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2022-01-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-12-27T18:19:52Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-12-15",
      "url": "https://rec98.nmlgc.net/blog/2021-12-15",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-12-15\"\u003e\u003ctime datetime=\"2021-12-15T23:21:58Z\"\u003e2021-12-15 23:21\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0170\"\u003eP0170\u003c/a\u003e\n\t\t\tWebsite (Technical debt visualization)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/0c4ab41...4f04091\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0171\"\u003eP0171\u003c/a\u003e\n\t\t\tWebsite (Resource caching + new funding goals)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/4f04091...e12cf26\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThe \"bad\" news first: Expanding to Stripe in order to support Google Pay\n\trequires bureaucratic effort that is not quite justified yet, and would only\n\tbe worth it after the next price increase.\n\u003c/p\u003e\u003cp\u003e\n\tVisualizing technical debt has definitely been overdue for a while though.\n\tWith 1 of these 2 pushes being focused on this topic, it makes sense to\n\tsummarize once again what \"\u003ca\n\thref=\"https://en.wikipedia.org/wiki/Technical_debt\"\u003etechnical debt\u003c/a\u003e\"\n\tmeans in the context of ReC98, as this info was previously kind of scattered\n\tover multiple blog posts. Mainly, it encompasses\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eany ZUN-written code\u003c/li\u003e\n\t\u003cli\u003ethat we did name and reverse-engineer,\u003c/li\u003e\n\t\u003cli\u003ebut which we simply moved out into dedicated files that are then\n\t\u003ccode\u003e#include\u003c/code\u003ed back into the big .ASM translation units,\u003c/li\u003e\n\t\u003cli\u003ewithout worrying about decompilation or proving undecompilability for\n\tnow.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t\u003ci\u003eTechnically\u003c/i\u003e (ha), it would also include all of master.lib, which has\n\talways been compiled into the binaries in this way, and which will require\n\tquite a bit of dedicated effort to be moved out into a properly linkable\n\tlibrary, once it's feasible. But this code has never been part of any\n\tprogress metric – in fact, \u003ca href=\"/progress/re-baseline\"\u003e0% RE\u003c/a\u003e is\n\tdefined as the total number of x86 instructions in the binary \u003ci\u003eminus\u003c/i\u003e\n\tany library code. There is also no relation between instruction numbers and\n\tthe time it will take to finalize master.lib code, let alone a precedent of\n\thow much it would cost.\n\u003c/p\u003e\u003cp\u003e\n\tIf we now want to express technical debt as a percentage, it's clear where\n\tthe 100% point would be: when all RE'd code is also compiled in from a\n\ttranslation unit outside the big .ASM one. But where would 0% be? Logically,\n\tit would be the point where no reverse-engineered code has ever been moved\n\tout of the big translation units yet, and nothing has ever been decompiled.\n\tWith these boundary points, this is what we get:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2021-12-15-Repaid.png?71e09488\"\u003e\u003cimg src=\"/blog/static/2021-12-15-Repaid.png?71e09488\" alt=\"Visualizing technical debt in terms of the total amount of instructions that could possibly be not finalized\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNot too bad! So it's 6.22% of total RE that we will have to revisit at some\n\tpoint, concentrated mostly around TH04 and TH05 where it resulted from a\n\tfocus on position independence. The prices also give an accurate impression\n\tof how much more work would be required there.\n\u003c/p\u003e\u003cp\u003e\n\tBut is that really the best visualization? After all, it requires an\n\tunderstanding of our definition of technical debt, so it's maybe not the\n\tmost useful measurement to have on a front page. But how about subtracting\n\tthose 6.22% from the number shown on the RE% bars? Then, we get this:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2021-12-15-Finalized.png?e6d4fd2c\"\u003e\u003cimg src=\"/blog/static/2021-12-15-Finalized.png?e6d4fd2c\" alt=\"Visualizing technical debt in terms of the absolute number of 'finalized' instructions per binary\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhich is where we get to the good news: Twitter surprisingly helped me out\n\tin choosing one visualization over the other, \u003ca\n\thref=\"https://twitter.com/ReC98Project/status/1470109509699973125\"\u003evoting\n\t7:2 in favor of the \u003ci\u003eFinalized\u003c/i\u003e version\u003c/a\u003e. While this one requires\n\tyou to manually calculate \u003ci\u003e€\u0026nbsp;finalized\u0026nbsp;-\u0026nbsp;€\u0026nbsp;RE'd\u003c/i\u003e to\n\tobtain the raw financial cost of technical debt, it clearly shows, for the\n\tfirst time, how far away we are from the main goal of fully decompiling all\n\t5 games… at least to the extent it's possible.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNow that the parser is looking at these recursively included .ASM files for\n\tthe first time, it needed a small number of improvements to correctly handle\n\tthe more advanced directives used there, which no automatic disassembler\n\twould ever emit. Turns out I've been counting some directives as\n\tinstructions that never should have been, which is where the additional\n\t0.02% total RE came from.\n\u003c/p\u003e\u003cp\u003e\n\tOne more overcounting issue remains though. Some of the RE'd assembly slices\n\tincluded by multiple games contain different \u003ccode\u003eif\u003c/code\u003e branches for\n\teach game, like this:\n\u003c/p\u003e\u003cpre\u003e; An example assembly file included by both TH04's and TH05's MAIN.EXE:\nif (GAME eq 5)\n\t; (Code for TH05)\nelse\n\t; (Code for TH04)\nendif\u003c/pre\u003e\u003cp\u003e\n\tCurrently, the parser simply ignores \u003ccode\u003eif\u003c/code\u003e, \u003ccode\u003eelse\u003c/code\u003e, and\n\t\u003ccode\u003eendif\u003c/code\u003e, leading to the combined code of all branches being\n\tcounted for every game that includes such a file. This also affects the\n\tcalculated speed, and is the reason why finalization seems to be slightly\n\tfaster than reverse-engineering, at currently 471 instructions per push\n\tcompared to 463. However, it's not that bad of a signal to send: Most of the\n\tnot yet finalized code is shared between TH04 and TH05, so finalizing it\n\twill roughly be twice as fast as regular reverse-engineering to begin with.\n\t(Unless the code then turns out to be twice as complex than average code…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e).\n\u003c/p\u003e\u003cp\u003e\n\tFor completeness, finalization is now also shown as part of the \u003ca\n\thref=\"/progress\"\u003eper-commit metrics\u003c/a\u003e. Now it's clearly visible what I was\n\tdoing in those very slow five months between \u003ca\n\thref=\"/progress/dc9e3ee47599dc02d737602c22a5bfa75eeaae34\"\u003eP0131\u003c/a\u003e and \u003ca\n\thref=\"/progress/d9858113d8135d265b88e0325d40fc237e6b9763\"\u003eP0140\u003c/a\u003e, where\n\tthe progress bar didn't move at all: Repaying 3.49% of previously\n\taccumulated technical debt across all games. 👌\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs announced, I've also implemented a new caching system for this website,\n\tas the second main feature of these two pushes. By appending a hash string\n\tto the URLs of static resources, your browser should now both cache them\n\tforever \u003ci\u003eand\u003c/i\u003e re-download them once they did change on the server. This\n\tavoids the unnecessary (and quite frankly, embarrassing) re-requests for all\n\tstatic resources that typically just return a \u003ccode\u003e304 Not Modified\u003c/code\u003e\n\tresponse. As a result, the blog should now load a bit faster on repeated\n\tvisits, especially on slower connections. That should allow me to\n\tdeliberately not paginate it for another few years, without it getting all\n\t\u003ci\u003etoo\u003c/i\u003e slow – and should prepare us for the day when our first game\n\treaches 100% and the server will get smashed. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\tHowever, I am open to changing the \u003ci\u003eprogress blog\u003c/i\u003e link in the\n\tnavigation bar at the top to the \u003ca href=\"/blog/tag\"\u003elist of tags\u003c/a\u003e, once\n\tpeople start complaining.\n\u003c/p\u003e\u003cp\u003e\n\tApart frome some more invisible correctness and QoL improvements, I've also\n\tprepared some new funding goals, but I'll cover those once the store\n\treopens, next year. Syntax highlighting for code snippets would have also\n\tbeen cool, but unfortunately didn't make it into those two pushes. It's\n\tstill on the list though!\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Back to RE with the TH03 score file format, and other code that\n\tsurrounds it.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-12-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-12-15T23:21:58Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-12-01",
      "url": "https://rec98.nmlgc.net/blog/2021-12-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-11-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-12-01\"\u003e\u003ctime datetime=\"2021-12-01T15:00:00Z\"\u003e2021-12-01\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tMade it through almost three years without a price increase! It's been\n\toverdue for a while, though.\n\u003c/p\u003e\u003cp\u003e\n\tWith the last months being full of rather research- and documentation-heavy\n\tpushes, I've been just about able to keep up with the existing\n\tsubscriptions. By now, the amount of quality control and documentation I\n\tfound myself putting into this project has far surpassed the raw\n\treverse-engineering work. Back at the beginning of 2019 when I decided on\n\tthe previous push price of 30 €, I didn't have this blog nor the current\n\taspirations at code quality. Neither of these have ever been reflected in\n\tthe price, and I still find it hard to put a number on them. On the other\n\thand, I continue to dislike the typical Patreon model of no inherent defined\n\tobligations on my part, and no direct association of the resulting work with\n\tthe person who funded it. You might have noticed that I don't use the word\n\t\"donations\" anywhere, and instead refer to them as \"orders\" or \"purchases\" –\n\tand that's precisely for this reason.\u003cbr\u003e\n\tThe result, however, has been a sold-out store for pretty much all of 2021.\n\tI can only begin to imagine how much potential revenue I've already lost\n\tfrom people who might have wanted to contribute at one point, but couldn't,\n\tand have already written off this project…\n\u003c/p\u003e\u003cp\u003e\n\tRaising prices is pretty much the only way to get the pending workload back\n\tto a more comfortable amount. I also thought about a two-tiered system: Have\n\ta documentation-less option for 30 €, and take 60 € for any push that should\n\tbe accompanied by a blog post. However, skimping on documentation will\n\tcompromise the quality of the code as well. Writing these blog posts\n\tpresents another chance of improving it before release, which has made quite\n\ta difference on many occasions. And after all, this documentation is the one\n\tthing about ReC98 that people mainly interact with. As long as we haven't\n\thit 100% RE, the actual code seems to be an afterthought, which is perfectly\n\tunderstandable: Why start work on a bigger mod or port now if the code is\n\tsteadily improving in every aspect, and it all will be just a bit more\n\tmaintainable in a few months?\n\u003c/p\u003e\u003cp\u003e\n\tBut why go more commercial then, and especially now? If the recent attention\n\tto \u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e's PC-98 Touhou\n\tcollection package is any indication, ReC98 has a way bigger career\n\tpotential than the dead-end RL job I found myself in. Demand for \u003ca\n\thref=\"https://twitter.com/HeroOnTheWind/status/1456608993783189507\"\u003efixed\n\ttranslations\u003c/a\u003e and \u003ca\n\thref=\"https://twitter.com/berunto88/status/1456359566543835136\"\u003ereplay\n\tsupport\u003c/a\u003e is definitely there – and given that these haven't been done so\n\tfar, it's very likely that I'll end up as the one to implement such mods,\n\tespecially if that should happen before reaching 100% RE or PI. People also\n\t\u003cspan\n\tclass=\"hovertext\"\n\ttitle=\"Source: Multiple Discord messages from people in the PC-98 / vintage hardware scene\"\n\t\u003e\u003ci\u003estill\u003c/i\u003e seem to want*\u003c/span\u003e a port to IBM-compatible DOS,\n\t\u003ca href=\"/blog/2020-05-04\"\u003e📝 even though this makes no sense\u003c/a\u003e? But if\n\tthis is something you all want to pay for, then sure, why not.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tAnd even right now, working on ReC98 sure beats writing junk software using\n\till-suited technologies for highly corporate clients, or living close to a\n\tworld where academic papers are valued higher than working and maintained\n\tcode. I am in fact very happy whenever I'm done with that for the day, and\n\tget to work on ReC98! Who would have thought.\n\u003c/p\u003e\u003cp\u003e\n\tSo let's try to grow this into an actual business and raise prices to match\n\tdemand, going up to a nicely divisible 60 € per push. If you all still\n\tmanage to regularly sell out the store at \u003ci\u003ethis\u003c/i\u003e level and I get to\n\traise prices again, I should be able to reduce RL work further and therefore\n\traise the cap as well. Now that I've also clarified a potential route\n\ttowards self-employment, I'm going to react to these sell-out events more\n\tquickly, and with smaller raises. So, no further immediate doubling in the\n\tfuture.\n\u003c/p\u003e\u003cp\u003e\n\tNow, will this delay the currently highly awaited 100% completion of TH01\n\tpast August 2022, the 25th anniversary of its release? We'll see once we're\n\tback to an almost empty backlog, after I'm done with the TH01 Sariel fight.\n\tI'm hopeful that such a price increase will give a new voice to the goals\n\tand priorities of less wealthy potential patrons. This crowdfunding is very\n\tmuch designed to be hacked by \"microtransactions\" – small contributions with\n\tspecific requests that require other, larger generic contributions to be\n\tfulfilled – and I'd like to see more of that. 😛 And even if 60 € per push\n\tis already more than the combined fandom wants to pay, that means I can get\n\tthe \u003ca href=\"/blog/2020-09-03\"\u003e📝 16-bit build system\u003c/a\u003e done before the\n\tfirst big 100% release. (Trust me, you really want that!)\n\u003c/p\u003e\u003cp\u003e\n\tI will still deliver the entire current backlog at the value the\n\tcontributions were originally purchased at. Due to the way the cap has to be\n\tcalculated, these contributions now appear to have doubled in value. All\n\texisting subscriptions will then pay for half of their original pushes\n\tstarting with their respective December 2021 transaction.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: A bunch of smaller website features, including:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003ea caching strategy for static content,\u003c/li\u003e\n\t\u003cli\u003ea third set of percentage bars to visualize the remaining technical\n\tdebt,\u003c/li\u003e\n\t\u003cli\u003eand, hopefully, some new payment methods that expand the number of\n\tcountries I can accept money from. (Yes, this was requested at one point\n\tearlier this year!)\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-11-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-12-01T16:00:00+01:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-11-29",
      "url": "https://rec98.nmlgc.net/blog/2021-11-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-11-08\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-11-29\"\u003e\u003ctime datetime=\"2021-11-29T01:56:18Z\"\u003e2021-11-29 01:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0168\"\u003eP0168\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (EMS swap area, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c2de6ab...8b046da\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0169\"\u003eP0169\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (EMS swap area, part 2/2) + Research (TH04 No-EMS Reimu Stage 5 crash)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8b046da...479b766\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003erosenrose, Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuuka-5\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 5 boss.\"\u003eyuuka-5\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Expanded_memory\"\u003eEMS memory\u003c/a\u003e! The\n\tinfamous stopgap measure between the 640 KiB (\"ought to be enough for\n\teveryone\") of \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Conventional_memory\"\u003econventional\n\tmemory\u003c/a\u003e offered by DOS from the very beginning, and the later \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Extended_memory\"\u003eXMS standard\u003c/a\u003e for\n\taccessing all the rest of memory up to 4 GiB in the x86 Protected Mode. With\n\tan optionally active EMS driver, TH04 and TH05 will make use of EMS memory\n\tto preload a bunch of situational .CDG images at the beginning of\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eThe \"eye catch\" game title image, shown while stages are loaded\u003c/li\u003e\n\t\u003cli\u003eThe character-specific background image, shown while bombing\u003c/li\u003e\n\t\u003cli\u003eThe player character dialog portraits\u003c/li\u003e\n\t\u003cli\u003eTH05 additionally stores the boss portraits there, preloading them\n\tat the beginning of each stage. (TH04 instead keeps them in conventional\n\tmemory during the entire stage.)\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tOnce these images are needed, they can then be copied into conventional\n\tmemory and accessed as usual.\n\u003c/p\u003e\u003cp\u003e\n\tUh… wait, \u003ci\u003ecopied\u003c/i\u003e? It certainly would have been possible to map EMS\n\tmemory to a regular 16-bit Real Mode segment for direct access,\n\tbank-switching out rarely used system or peripheral memory in exchange for\n\tthe EMS data. However, master.lib doesn't expose this functionality, and\n\tonly provides functions for copying data from EMS to regular memory and vice\n\tversa.\u003cbr\u003e\n\tBut even that still makes EMS an excellent fit for the large image files\n\tit's used for, as it's possible to directly copy their pixel data from EMS\n\tto VRAM. (Yes, I tried!) Well… \u003ci\u003ewould\u003c/i\u003e, because ZUN doesn't do\n\t\u003ci\u003ethat\u003c/i\u003e either, and always naively copies the images to newly allocated\n\tconventional memory first. In essence, this dumbs down EMS into just another\n\tlayer of the memory hierarchy, inserted between conventional memory and\n\tdisk: Not quite as slow as disk, but still requiring that\n\t\u003ccode\u003ememcpy()\u003c/code\u003e to retrieve the data. Most importantly though: Using\n\tEMS in this way does \u003ci\u003enot\u003c/i\u003e increase the total amount of memory\n\tsimultaneously accessible to the game. After all, some other data will have\n\tto be freed from conventional memory to make room for the newly loaded data.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe most idiomatic way to define the game-specific layout of the EMS area\n\twould be either a \u003ccode\u003estruct\u003c/code\u003e or an \u003ccode\u003eenum\u003c/code\u003e.\n\tUnfortunately, the total size of all these images exceeds the range of a\n\t16-bit value, and Turbo C++ 4.0J supports neither 32-bit \u003ccode\u003eenum\u003c/code\u003es\n\t(which are silently degraded to 16-bit) nor 32-bit \u003ccode\u003estruct\u003c/code\u003es\n\t(which simply don't compile). That still leaves raw compile-time constants\n\tthough, you only have to manually define the offset to each image in terms\n\tof the size of its predecessor. But instead of doing that, ZUN just placed\n\teach image at a nice round decimal offset, each slightly larger than the\n\tactual memory required by the previous image, just to make sure that\n\teverything fits. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e This results not only in quite\n\ta bit of unnecessary padding, but also in \u003ci\u003etechnically\u003c/i\u003e the single\n\tbiggest amount of \"wasted\" memory in PC-98 Touhou: Out of the 180,000 (TH04)\n\tand 320,000 (TH05) EMS bytes requested, the game only uses 135,552 (TH04)\n\tand 175,904 (TH05) bytes. But hey, it's EMS, so who cares, right? Out of all\n\tthe opportunities to take shortcuts during development, this is among the\n\tmost acceptable ones. Any actual PC-98 model that could run these two games\n\tcomes with plenty of memory for this to not turn into an actual issue.\n\u003cp\u003e\u003cp\u003e\n\tOn to the EMS-using functions themselves, which are the definition of\n\t\"cross-cutting concerns\". Most of these have a fallback path for the non-EMS\n\tcase, and keep the loaded .CDG images in memory if they are immediately\n\tneeded. Which totally makes sense, but also makes it difficult to find names\n\tthat reflect all the global state changed by these functions. Every one of\n\tthese is also just called from a single place, so \u003ca\n\thref=\"http://number-none.com/blow/john_carmack_on_inlined_code.html\"\u003einlining\n\tthem\u003c/a\u003e would have saved me a lot of naming and documentation trouble\n\tthere.\u003cbr\u003e\n\tThe TH04 version of the EMS allocation code was actually \u003ca\n\thref=\"https://youtu.be/g3C5jMbTtps?t=577\"\u003edisplayed on ZUN's monitor in the\n\t2010 MAG・ネット documentary\u003c/a\u003e; \u003ca\n\thref=\"https://twitter.com/WindowsTiger\"\u003eWindowsTiger\u003c/a\u003e already \u003ca\n\thref=\"https://pastebin.com/hgPQTBqW\"\u003etranscribed the low-quality video image\n\tin 2019\u003c/a\u003e. By 2015 ReC98 standards, I would have just run with that, but\n\tthe current project goal is to write better code than ZUN, so I didn't. 😛\n\tWe sure ain't going to use magic numbers for EMS offsets.\n\u003c/p\u003e\u003cp\u003e\n\tThe dialog init and exit code then is completely different in both games,\n\tyet equally cross-cutting. TH05 goes even further in saving conventional\n\tmemory, loading each individual player or boss portrait into a single .CDG\n\tslot immediately before blitting it to VRAM and freeing the pixel data\n\tagain. People who play TH05 without an active EMS driver are surely going to\n\tenjoy the hard drive access lag between each portrait change…\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e TH04, on the other hand, also abuses the dialog\n\texit function to preload the Mugetsu defeat / Gengetsu entrance and\n\tGengetsu defeat portraits, using a static variable to track how often the\n\tfunction has been called during the Extra Stage… who needs function\n\tparameters anyway, right? \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tThis is also the function in which TH04 infamously crashes after the Stage 5\n\tpre-boss dialog when playing with Reimu and without any active EMS driver.\n\tThat crash is what motivated this look into the games' EMS usage… but the\n\tcode looks perfectly fine? Oh well, guess the crash is not related to EMS\n\tthen. Next u–\n\u003c/p\u003e\u003cp\u003e\n\tOK, of course I can't leave it like that. Everyone is expecting a fix now,\n\tand I still got half of a push left over after decompiling the regular EMS\n\tcode. Also, I've now RE'd every function that could possibly be involved in\n\tthe crash, and this is very likely to be the last time I'll be looking at\n\tthem.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tTurns out that the bug has little to do with EMS, and everything to do with\n\tZUN limiting the amount of conventional RAM that TH04's\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e is allowed to use, and then slightly miscalculating\n\tthis upper limit. Playing Stage 5 with Reimu is the most asset-intensive\n\tconfiguration in this game, due to the combination of\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e6 player portraits (Marisa has only 5), at 128×128 pixels each\u003c/li\u003e\n\t\u003cli\u003ea 288×256 background for the boss fight, tied in size only with the\n\tones in the Extra Stage\u003c/li\u003e\n\t\u003cli\u003ethe additional 96×80 image for the vertically scrolling stars during\n\tthe stage, wastefully stored as 4 bitplanes rather than a single one.\n\tThis image is never freed, not even at the end of the stage.\u003c/li\u003e\n\u003c/ul\u003e\u003cfigure class=\"checkerboard\"\u003e\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABQAQMAAADcLOLWAAAABlBMVEVVABAAAACpPXcUAAAAAXRSTlMAQObYZgAAAOJJREFUeF6tkjFuxSAQRJ9FQekLROIaqcLFouCj+Sg+gksXlifFMlL+V5xPkS3wPgnPzgAwWkBa3QF5G4J5v4NyDEE9h6BddyCNwCQtv0OS1p/gdJAlB4JZsm0oks1BkwdBkbyvqdcFuJf+hId/iuGI8VF7uIxaI0zUguVsqEZ/2prFLNfjJYthOZ/PA2DlF9DUAzxD+Dn/C+7mjBq9S/p0IEna/EZI0jIZsi5o/cFknVA7zNr7AszaIBu0wNShCKBfSbkAWofDK1B3gDly1xUgBbQFYIrcX/gDfAZ8APAe8AbfrNNSOKJbw2wAAAAASUVORK5CYII=\"\n\t\talt=\"The star image used in TH04's Stage 5.\"\u003e\n\t\u003cfigcaption\u003eThe star image used in TH04's Stage 5.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tRemove any single one of the above points, and this crash would have never\n\toccurred. But with all of them combined, the total amount of memory consumed\n\tby TH04's \u003ccode\u003eMAIN.EXE\u003c/code\u003e just barely exceeds ZUN's limit of 320,000\n\tbytes, by no more than 3,840 bytes, the size of the star image.\n\u003c/p\u003e\u003cp\u003e\n\tBut wait: As we established earlier, EMS does nothing to reduce the amount\n\tof conventional memory used by the game. In fact, if you disabled TH04's EMS\n\thandling, you'd still get this crash even if you \u003ci\u003eare\u003c/i\u003e running an EMS\n\tdriver and loaded DOS into the High Memory Area to free up as much\n\tconventional RAM as possible. How can EMS then prevent this crash in the\n\tfirst place?\n\u003c/p\u003e\u003cp\u003e\n\tThe answer: It's only because ZUN's usage of EMS bypasses the need to load\n\tthe cached images back out of the XOR-encrypted \u003ccode\u003e東方幻想.郷\u003c/code\u003e\n\tpackfile. Leaving aside the \u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"Please, don't ever do this! Think of all the constructive things that hackers could do in the time they spend reverse-engineering your packfile encryption!\"\u003egeneral\n\tstupidity of any game data file encryption*\u003c/span\u003e, master.lib's decryption\n\timplementation is also quite wasteful: It uses a separate buffer that\n\treceives fixed-size chunks of the file, before decrypting every individual\n\tbyte and copying it to its intended destination buffer. That really\n\tresembles the typical slowness of a C \u003ccode\u003efread()\u003c/code\u003e implementation\n\tmore than it does the highly optimized ASM code that master.lib purports to\n\tbe… And how large is this well-hidden decryption buffer? 4 KiB.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo, looking back at the game, here is what happens once the Stage 5\n\tpre-battle dialog ends:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eReimu's bomb background image, which was previously freed to make space\n\tfor her dialog portraits, has to be loaded back into conventional memory\n\tfrom disk\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eBB0.CDG\u003c/code\u003e is found inside the \u003ccode\u003e東方幻想.郷\u003c/code\u003e\n\tpackfile\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003efile_ropen()\u003c/code\u003e ends up allocating a 4 KiB buffer for the\n\tencrypted packfile data, getting us the decisive ~4 KiB closer to the memory\n\tlimit\u003c/li\u003e\n\t\u003cli\u003eThe .CDG loader tries to allocate 52\u0026nbsp;608 contiguous bytes for the\n\tpixel data of Reimu's bomb image\u003c/li\u003e\n\t\u003cli\u003eThis would exceed the memory limit, so \u003ccode\u003ehmem_allocbyte()\u003c/code\u003e\n\tfails and returns a \u003ccode\u003enullptr\u003c/code\u003e\u003c/li\u003e\n\t\u003cli\u003eZUN doesn't check for this case (as usual)\u003c/li\u003e\n\t\u003cli\u003eThe pixel data is loaded to address \u003ccode\u003e0000:0000\u003c/code\u003e,\n\toverwriting the Interrupt Vector Table and whatever comes after\u003c/li\u003e\n\t\u003cli\u003eThe game crashes\u003c/li\u003e\n\u003c/ol\u003e\u003cfigure\u003e\u003ca href=\"/blog/static/2021-11-29-TH04-NoEMS-Crash-screen.png?3c450464\"\u003e\u003cimg\n\tsrc=\"/blog/static/2021-11-29-TH04-NoEMS-Crash-screen.png?3c450464\"\n\talt=\"The final frame rendered before the TH04 Stage 5 Reimu No-EMS crash\"\n\u003e\u003c/a\u003e\n\t\u003cfigcaption\u003eThe final frame rendered by a crashing TH04.\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe 4 KiB encryption buffer would only be freed by the corresponding\n\t\u003ccode\u003efile_close()\u003c/code\u003e call, which of course never happens because the\n\tgame crashes before it gets there. At one point, I really did suspect the\n\tcause to be some kind of memory leak or fragmentation inside master.lib,\n\twhich would have been quite delightful to fix.\u003cbr\u003e\n\tInstead, the most straightforward fix here is to bump up that memory limit\n\tby at least 4 KiB. Certainly easier than squeezing in a\n\t\u003ccode\u003ecdg_free()\u003c/code\u003e call for the star image before the pre-boss dialog\n\twithout breaking position dependence.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ci\u003eOr\u003c/i\u003e, even better, let's nuke all these memory limits from orbit\n\tbecause they make little sense to begin with, and fix every other potential\n\tout-of-memory crash that modders would encounter when adding enough data to\n\tany of the 4 games that impose such limits on themselves. Unless you want to\n\tlaunch other binaries (which need to do their own memory allocations) after\n\tlaunching the game, there's really no reason to restrict the amount of\n\tmemory available to a DOS process. Heck, whenever DOS creates a new one, it\n\tassigns all remaining free memory by default anyway.\u003cbr\u003e\n\tRemoving the memory limits also removes one of ZUN's few error checks, which\n\tend up quitting the game if there isn't at least a given maximum amount of\n\tconventional RAM available. While it might be tempting to reserve enough\n\tmemory at the beginning of execution and then never check any allocation for\n\ta potential failure, that's \u003ci\u003eexactly\u003c/i\u003e where something like TH04's crash\n\tcomes from.\u003cbr\u003e\n\tThis game is also still running on DOS, where such an initial allocation\n\tfailure is very unlikely to happen – no one fills close to half of\n\tconventional RAM with TSRs and then tries running one of these games. It\n\t\u003ci\u003emight\u003c/i\u003e have been useful to detect systems with less than 640 KiB of\n\tactual, physical RAM, but none of the PC-98 models with that little amount\n\tof memory are fast enough to run these games to begin with. How ironic… a\n\tplace where ZUN actually added an error check, and then it's mostly\n\tpointless.\n\u003c/p\u003e\u003cp\u003e\n\tHere's an archive that contains both fix variants, just in case. These were\n\tcompiled from the \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/th04_noems_crash_fix\"\u003e\u003ccode\u003eth04_noems_crash_fix\u003c/code\u003e\u003c/a\u003e\n\tand \u003ca\n\thref=\"https://github.com/nmlgc/ReC98/tree/mem_assign_all\"\u003e\u003ccode\u003emem_assign_all\u003c/code\u003e\u003c/a\u003e\n\tbranches, and contain as little code changes as possible.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2022-04-18):\u003c/strong\u003e For TH04, you probably want to download\n\tthe \u003ca href=\"/blog/2022-04-18\"\u003e📝 community choice fix package\u003c/a\u003e instead,\n\twhich contains this fix along with other workarounds for the \u003ccode\u003eDivide\n\terror\u003c/code\u003e crashes.\n\t\u003ca class=\"download\" href=\"/blog/static/2021-11-29-Memory-limit-fixes.zip?07de8580\" data-kb=\"667.5\"\u003e2021-11-29-Memory-limit-fixes.zip \u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo yeah, quite a complex bug, leaving no time for the TH03 scorefile format\n\tresearch after all. Next up: Raising prices.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-12-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-11-08\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-11-29T01:56:18Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-11-08",
      "url": "https://rec98.nmlgc.net/blog/2021-11-08",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-11-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-10-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-11-08\"\u003e\u003ctime datetime=\"2021-11-08T10:59:48Z\"\u003e2021-11-08 10:59\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0165\"\u003eP0165\u003c/a\u003e\n\t\t\tTH01 decompilation (Missiles, part 1/2 + large boss sprites, part 1/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/7a0e5d8...f2bca01\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0166\"\u003eP0166\u003c/a\u003e\n\t\t\tTH01 decompilation (Large boss sprites, part 2/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f2bca01...e697907\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0167\"\u003eP0167\u003c/a\u003e\n\t\t\tTH01 decompilation (Large boss sprites, part 3/3 + Stage initialization + Defeat animation + Route selection)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e697907...c2de6ab\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/singyoku\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 5 boss.\"\u003esingyoku\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/elis\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 魔界/Makai route.\"\u003eelis\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tOK, TH01 missile bullets. Can we maybe have a well-behaved entity type,\n\twithout any weirdness? Just \u003ci\u003eonce\u003c/i\u003e?\n\u003c/p\u003e\u003cp\u003e\n\tEhh, kinda. Apart from another 150 bytes wasted on unused structure members,\n\tthis code is indeed more on the low end in terms of overall jank. It does\n\tbecome very obvious why dodging these missiles in the YuugenMagan, Mima, and\n\tElis fights feels so awful though: An unfair 46×46 pixel hitbox around\n\tReimu's center pixel, combined with the comeback of\n\t\u003ca href=\"/blog/2020-07-12\"\u003e📝 interlaced rendering\u003c/a\u003e, this time in every\n\tstage. ZUN probably did this because missiles are the only 16×16 sprite in\n\tTH01 that is blitted to unaligned X positions, which effectively ends up\n\ttouching a 32×16 area of VRAM per sprite.\u003cbr\u003e\n\tBut even \u003ci\u003eif\u003c/i\u003e we assume VRAM writes to be the bottleneck here, it would\n\thave been totally possible to render every missile in every frame at roughly\n\tthe same amount of CPU time that the original game uses for interlaced\n\trendering:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eNote that all missile sprites only use two colors, white and green.\u003c/li\u003e\n\t\u003cli\u003eInstead of naively going with the usual four bitplanes, extract the\n\tpixels drawn in each of the two used colors into their own bitplanes.\n\tmaster.lib calls this the \"tiny format\".\u003c/li\u003e\n\t\u003cli\u003eUse the GRCG to draw these two bitplanes in the intended white and green\n\tcolors, halving the amount of VRAM writes compared to the original\n\tfunction.\u003c/li\u003e\n\t\u003cli\u003e(Not using the .PTN format would have also avoided the inconsistency of\n\tstoring the missile sprites in boss-specific sprite slots.)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThat's an optimization that would have significantly benefitted the game, in\n\tcontrast to \u003ca href=\"/blog/tag/micro-optimization\"\u003eall of the fake ones\n\tintroduced in later games\u003c/a\u003e. Then again, this optimization \u003ci\u003eis\u003c/i\u003e\n\tactually something that the later games do, and it might have in fact been\n\tnecessary to achieve their higher bullet counts without significant\n\tslowdown.\n\u003c/p\u003e\u003cp\u003e\n\tUnfortunately, it was only worth decompiling half of the missile code right\n\tnow, thanks to gratuitous FPU usage in the other half, where\n\t\u003ca href=\"/blog/2020-06-13\"\u003e📝 \u003ccode\u003edouble\u003c/code\u003e variables are compared to \u003ccode\u003efloat\u003c/code\u003e literals\u003c/a\u003e.\n\tThat one will have to wait\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 until after SinGyoku\u003c/a\u003e.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter some effectively unused Mima sprite effect code that is so broken that\n\tit's impossible to make sense out of it, we get to the final feature I\n\twanted to cover for all bosses in parallel before returning to Sariel: The\n\tseparate sprite background storage for moving or animated boss sprites in\n\tthe Mima, Elis, and Sariel fights. But, uh… why is this necessary to begin\n\twith? Doesn't TH01 already reserve the other VRAM page for backgrounds?\n\t\u003cbr\u003e\n\tWell, these sprites are quite big, and ZUN didn't want to blit them from\n\tmain memory on every frame. After all, TH01 and TH02 had a minimum required\n\tclock speed of 33 MHz, half of the speed required for the later three games.\n\tSo, he simply blitted these boss sprites to \u003ci\u003eboth\u003c/i\u003e VRAM pages, leading\n\tthe usual unblitting calls to only remove the other sprites on top of the\n\tboss. However, these bosses themselves want to move across the screen…\n\tand this makes it necessary to save the stage background behind \u003ci\u003ethem\u003c/i\u003e\n\tin some other way.\n\u003c/p\u003e\u003cp\u003e\n\tEnter .PTN, and its functions to capture a 16×16 or 32×32 square from VRAM\n\tinto a sprite slot. No problem with that approach in theory, as the size of\n\tall these bigger sprites is a multiple of 32×32; splitting a larger sprite\n\tinto these smaller 32×32 chunks makes the code look just a little bit clumsy\n\t(and, of course, slower).\u003cbr\u003e\n\tBut somewhere during the development of Mima's fight, ZUN apparently forgot\n\tthat those sprite backgrounds existed. And once Mima's 🚫 casting sprite is\n\tblitted on top of her regular sprite, using just regular sprite\n\ttransparency, she ends up with her infamous third arm:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\u003ca href=\"/blog/static/2021-11-08-TH01-Mima-three-arms.png?214a7b81\"\u003e\u003cimg\n\tsrc=\"/blog/static/2021-11-08-TH01-Mima-three-arms.png?214a7b81\" alt=\"TH01 Mima's third arm\" style=\"height: 320px\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tIronically, there's an unused code path in Mima's unblit function where ZUN\n\tassumes a height of 48 pixels for Mima's animation sprites rather than the\n\tactual 64. This leads to even clumsier .PTN function calls for the bottom\n\t128×16 pixels… Failing to unblit the bottom 16 pixels would have also\n\tyielded that third arm, although it wouldn't have looked as natural. Still\n\twouldn't say that it was intentional; maybe this casting sprite was just\n\tadded pretty late in the game's development?\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, mission accomplished, Sariel unblocked… at 2¼ pushes. \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e That's quite some time left for some smaller stage initialization\n\tcode, which bundles a bunch of random function calls in places where they\n\tlogically really don't belong. The stage opening animation then adds a bunch\n\tof VRAM inter-page copies that are not only redundant but can't even be\n\tunderstood without knowing the hidden internal state of the last VRAM page\n\taccessed by previous ZUN code…\u003cbr\u003e\n\tIn better news though: Turbo C++ 4.0 really doesn't seem to have any\n\tcomplexity limit on inlining arithmetic expressions, as long as they only\n\toperate on compile-time constants. That's how we get macro-free,\n\tcompile-time Shift-JIS to JIS X 0208 conversion of the individual code\n\tpoints in the 東方★靈異伝 string, in a compiler from 1994. As long as you\n\tdon't store any intermediate results in variables, that is…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tBut wait, there's more! With still ¼ of a push left, I also went for the\n\tboss defeat animation, which includes the route selection after the SinGyoku\n\tfight.\u003cbr\u003e\n\tAs in all other instances, the 2× scaled font is accomplished by first\n\trendering the text at regular 1× resolution to the other, invisible VRAM\n\tpage, and then scaled from there to the visible one. However, the route\n\tselection is unique in that its scaled text is both drawn transparently on\n\ttop of the stage background (not onto a black one), and can also change\n\tcolors depending on the selection. It would have been no problem to unblit\n\tand reblit the text by rendering the 1× version to a position on the\n\tinvisible VRAM page that isn't covered by the 2× version on the visible one,\n\tbut ZUN (needlessly) clears the invisible page before rendering any text.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Instead, he assigned a separate VRAM color for both\n\tthe 魔界 and 地獄 options, and only changed the \u003ci\u003epalette\u003c/i\u003e value for\n\tthese colors to white or gray, depending on the correct selection. This is\n\tanother one of the\n\t\u003ca href=\"/blog/2020-12-18\"\u003e📝 rare cases where TH01 demonstrates good use of PC-98 hardware\u003c/a\u003e,\n\tas the 魔界へ and 地獄へ strings don't need to be reblitted during the selection process, only the Orb \"cursor\" does.\n\u003c/p\u003e\u003cp\u003e\n\tThen, why does this still not count as \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e? When\n\tchanging palette colors, you \u003ci\u003ekinda\u003c/i\u003e need to be aware of everything\n\telse that can possibly be on screen, which colors are used there, and which\n\taren't and can therefore be used for such an effect without affecting other\n\tsprites. In this case, well… hover over the image below, and notice how\n\tReimu's hair and the bomb sprites in the HUD light up when Makai is\n\tselected:\n\u003cfigure class=\"pixelated\"\u003e\u003cimg\n\tsrc=\"/blog/static/2021-11-08-TH01-Route-selection-Jigoku.png?7cdd9a09\"\n\tonmouseover=\"this.setAttribute('src', \u0026#34;/blog/static/2021-11-08-TH01-Route-selection-Makai.png?f00c814e\u0026#34;);\"\n\tonmouseout =\"this.setAttribute('src', \u0026#34;/blog/static/2021-11-08-TH01-Route-selection-Jigoku.png?7cdd9a09\u0026#34;);\"\n\talt=\"Demonstration of palette changes in TH01's route selection\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThis push did end on a high note though, with the generic, non-SinGyoku\n\tversion of the defeat animation being an easily parametrizable copy. And\n\tthat's how you decompile another 2.58% of TH01 in just slightly over three\n\tpushes.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNow, we're not only ready to decompile Sariel, but also Kikuri, Elis, and\n\tSinGyoku without needing any more detours into non-boss code. Thanks to the\n\tcurrent TH01 funding subscriptions, I can plan to cover most, if not all, of\n\tSariel in a single push series, but the currently 3 pending pushes probably\n\twon't suffice for Sariel's 8.10% of all remaining code in TH01. We've got\n\tquite a lot of not specifically TH01-related funds in the backlog to pass\n\tthe time though.\n\u003c/p\u003e\u003cp\u003e\n\tDue to recent developments, it actually makes quite a lot of sense to take a\n\tbreak from TH01: \u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e has\n\tmanaged what every Touhou download site so far has failed to do: Bundling\n\tall 5 game onto a single .HDI together with \u003ca\n\thref=\"https://github.com/nmlgc/np2debug/commit/a40ad5c\"\u003epre-configured PC-98\n\temulators\u003c/a\u003e and a nice boot menu, and hosting the resulting package on a\n\tproper website. While this first release is already quite good (and much\n\tbetter than my attempt from 2014), there is still a bit of room for\n\timprovement to be gained from specific ReC98 research. Next up,\n\ttherefore:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eResearching how TH04 and TH05 use EMS memory, together with the cause\n\tbehind TH04's crash in Stage 5 when playing as Reimu without an EMS driver\n\tloaded, and\u003c/li\u003e\n\t\u003cli\u003ereverse-engineering TH03's score data file format\n\t(\u003ccode\u003eYUME.NEM\u003c/code\u003e), which hopefully also comes with a way of building a\n\tfile that unlocks all characters without any high scores.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-11-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-10-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-11-08T10:59:48Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-10-20",
      "url": "https://rec98.nmlgc.net/blog/2021-10-20",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-11-08\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-10-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-10-20\"\u003e\u003ctime datetime=\"2021-10-20T09:58:26Z\"\u003e2021-10-20 09:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0162\"\u003eP0162\u003c/a\u003e\n\t\t\tTH01 decompilation (Player control, part 1/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/81dd96e...24b3a0d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0163\"\u003eP0163\u003c/a\u003e\n\t\t\tTH01 decompilation (Player control, part 2/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/24b3a0d...6d572b3\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0164\"\u003eP0164\u003c/a\u003e\n\t\t\tTH01 decompilation (Player control, part 3/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6d572b3...7a0e5d8\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNo technical obstacles for once! Just pure overcomplicated ZUN code. Unlike\n\t\u003ca href=\"/blog/2021-08-23\"\u003e📝 Konngara's main function\u003c/a\u003e, the main TH01\n\tplayer function was every bit as difficult to decompile as you would expect\n\tfrom its size.\n\u003c/p\u003e\u003cp\u003e\n\tWith TH01 using both separate left- and right-facing sprites for all of\n\tReimu's moves \u003ci\u003eand\u003c/i\u003e separate classes for Reimu's 32×32 and 48×*\n\tsprites, we're already off to a bad start. Sure, sprite mirroring is\n\tminimally more involved on PC-98, as the \u003ca\n\thref=\"https://en.wikipedia.org/wiki/Planar_(computer_graphics)\"\u003eplanar\n\tnature of VRAM\u003c/a\u003e requires the bits within an 8-pixel byte to also be\n\tmirrored, in addition to writing the sprite bytes from right to left. TH03\n\tuses a 256-byte lookup table for this, generated at runtime by an infamous\n\tmicro-optimized and undecompilable ASM algorithm. With TH01's existing\n\tarchitecture, ZUN would have then needed to write 3 additional blitting\n\tfunctions. But instead, he chose to waste a total of 26,112 bytes of memory\n\ton pre-mirrored sprites… \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAlright, but surely selecting those sprites from code is no big deal? Just\n\tstore the direction Reimu is facing in, and then add some branches to the\n\trendering code. And there is in fact a variable for Reimu's direction…\n\tduring regular arrow-key movement, and \u003ci\u003eanother\u003c/i\u003e one while shooting and\n\tsliding, and a \u003ci\u003ethird\u003c/i\u003e as \u003ci\u003epart\u003c/i\u003e of the special attack types,\n\tlaunched out of a slide.\u003cbr\u003e\n\tWell, OK, technically, the last two are the same variable. But that's even\n\tworse, because it means that ZUN stores two distinct \u003ccode\u003eenum\u003c/code\u003es at\n\tthe same place in memory: Shooting and sliding uses \u003cvar\u003e1\u003c/var\u003e for left,\n\t\u003cvar\u003e2\u003c/var\u003e for right, and \u003cvar\u003e3\u003c/var\u003e for the \"invalid\" direction of\n\tholding both, while the special attack types indicate the direction in their\n\tlowest bit, with \u003cvar\u003e0\u003c/var\u003e for right and \u003cvar\u003e1\u003c/var\u003e for left. I\n\tdecompiled the latter as bitflags, but in ZUN's code, each of the 8\n\tpermutations is handled as a distinct type, with copy-pasted and adapted\n\tcode… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e The interpretation of this\n\ttwo-\u003ccode\u003eenum\u003c/code\u003e \"sub-mode\" \u003ccode\u003eunion\u003c/code\u003e variable is controlled\n\tby yet another \"mode\" variable… and unsurprisingly, two of the bugs in this\n\tfunction relate to the sub-mode variable being interpreted incorrectly.\n\u003c/p\u003e\u003cp\u003e\n\tAlso, \"rendering code\"? This one big function basically consists of separate\n\tunblit→update→render code snippets for every state and direction Reimu can\n\tbe in (moving, shooting, swinging, sliding, special-attacking, and bombing),\n\tpasted together into a tangled mess of nested \u003ccode\u003eif(…)\u003c/code\u003e statements.\n\tWhile a lot of the code is copy-pasted, there are still a number of\n\tinconsistencies that defeat the point of my usual refactoring treatment.\n\tAfter all, with a total of 85 conditional branches, anything more than I did\n\twould have just obscured the control flow too badly, making it even harder\n\tto understand what's going on.\u003cbr\u003e\n\tIn the end, I spotted a total of 8 bugs in this function, all of which leave\n\tReimu invisible for one or more frames:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e2 frames after all special attacks\u003c/li\u003e\n\t\u003cli\u003e2 frames after swing attacks, and\u003c/li\u003e\n\t\u003cli\u003e\u003ci\u003e4 frames\u003c/i\u003e before swing attacks\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThanks to the last one, Reimu's first swing animation frame is never\n\tactually rendered. So whenever someone complains about TH01 sprite\n\tflickering on an emulator: That emulator is accurate, it's the game that's\n\tpoorly written. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAnd guess what, this function doesn't even contain everything you'd\n\tassociate with per-frame \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/player\" title=\"Player-controlled characters.\"\u003eplayer\u003c/a\u003e\u003c/span\u003e behavior. While it does\n\thandle Yin-Yang Orb repulsion as part of slides and special attacks, it does\n\tnot handle the actual player/Orb collision that results in lives being lost.\n\tThe funny thing about this: These two things are done in the same function…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tTherefore, the life loss animation is also part of another function. This is\n\twhere we find the final glitch in this 3-push series: Before the 16-frame\n\tshake, this function only unblits a 32×32 area around Reimu's center point,\n\teven though it's possible to lose a life during the non-deflecting part of a\n\t48×48-pixel animation. In that case, the extra pixels will just stay on\n\tscreen during the shake. They are unblitted afterwards though, which\n\tsuggests that ZUN was at least somewhat aware of the issue?\u003cbr\u003e\n\tFinally, the chance to see the alternate life loss sprite \u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhIAAgAPIGAPHxoaEBAfEBAfHx8aGhoQEBAQGB/wAAACH5BAUAAAYALAAAAAAgACAAAAPWaLrc/jDKSZuodSjBOYZDuHTfo4mD55XGtYQiA2tTt8JNoe9FfZ84Q4GjIzZorQsnZmDuloKerMKrClJSCVNYJRB2WEfwxOgSQgVYVkFDNsvVtC74qj/ivJl7eucWAH9zM2J7DjoAiGh6YlSBcmloLAuHiIBzOix4jlUfOmcDX5qYjXqhBaY7jV4zZ55ePI2nnyGhtGBrEDxeZ6GfnpATOgGip54DchJFAcMFzM3Oram5RRzMAR3Oj6N3UTbW2Nlc0zbV1tfVo7g5RUPlwtip6obEeEIKCQA7\"\n\talt=\"Alternate TH01 life loss sprite\"\u003e is exactly ⅛.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs for any new insights into game mechanics… you know what? I'm just not\n\tgoing to write anything, and leave you with this flowchart instead. Here's\n\tthe definitive guide on how to control Reimu in TH01 we've been waiting for\n\t24 years:\n\u003c/p\u003e\u003cfigure class=\"large\"\u003e\u003cembed\n\tsrc=\"/blog/static/2021-10-20-TH01-player-control.svg?f94a5431\"\n\talt=\"Definitive flowchart for how to control Reimu in TH01\"\u003e\n\t\u003cfigcaption\u003e\u003ca href=\"/blog/static/2021-10-20-TH01-player-control.svg?f94a5431\"\u003e(SVG download)\u003c/a\u003e\u003c/figcaption\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tPellets are deflected during all \u003cspan style=\"color: #aaa\"\u003egray\u003c/span\u003e\n\tstates. Not shown is the obvious \"double-tap Z and X\" transition from\n\tall non-(#1) states to the Bomb state, but that would have made this\n\tdiagram even more unwieldy than it turned out. And yes, you can shoot\n\ttwice as fast while moving left or right.\n\u003c/p\u003e\u003cp\u003e\n\tWhile I'm at it, here are two more animations from \u003ccode\u003eMIKO.PTN\u003c/code\u003e\n\twhich aren't referenced by any code:\n\u003c/p\u003e\u003cfigure class=\"side_by_side pixelated checkerboard\"\u003e\u003cimg\nsrc=\"data:image/gif;base64,R0lGODlhIAAgAPIFAAAAAPDw8KCgoPAAAKAAAP//AAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDQAFACwAAAAAIAAgAAAD41i6OwwQsknpuAvcIVWsVuE4hbZFEAcyW7CVGydfwMoWgevFmqnaC9cgUEKhTsAgCSVICWidJNEDEFibHFMtqQgwBQFwD7uVArxVcM7LYf+AXjTYuj6j37Y45Mq/fsw5aU1pglFwdXuJimWHgUaDKFxdOZBGkVNwZ1aWll04mBRxYZxMW2ugDKJopGemaxWirBFqriCxshCBgGwRp7y6eV5iJ6dawLZrwydGbV7BlFhQnMfIRbhp1Ek+nHTOkjAXBBEEWVXekiYE6gDqNCVi6Bvq5O0/hkDpOfT1jPGn5KfcgUgAACH5BAkNAAUALAUAAAAVACAAAAPFWFqzBRC6yUZTwA6JefuYtkUZUA2B9oilZjJMGrqbu1hBR0b2XQQkAWQgsEQcuR9AwBSOjJNIgDkFbpwvDHAZ6HZL1s0CsBV0zd9vqVM2N99HLbBJZi7tWbJ6CdnyOQ9eO2SDDnpCg35ZWniJcWNng30QQoaCkoRAkIqYgnKYJF5JeiV9ohmiGGYZRqc4XqphRjsoX6pOs5K2OqCUaIsruUG/URoEEQQjXMCsBM4AzlDLY8bR0WIPi81dyc7J2MWiyaJQCgkAIfkECQ0ABQAsBQAAABUAIAAAA8pYWrMFELrJRlPADol5+5i2RRlQDYH2iKVmMkUAr+7mLpb8kOSNyyQBZCCwRCZAgGApHBkngABkGRBIN80XRlq1Br6l62YRjVqpYHCpw2W6mcet9DwVwrVRdXUaEXIeX1JlfDwOeX6DPFpbSxGCilBfiI9Bi3mPlI54aiSZXi95ipmhciUQgWAbYBhWGUaoKapSrGI5gUOkD40uqJ2zHaKUaIs0IzzDUBoEEQTGrckDBNIA0k9Kv8XSzdVjO4YWBF/b3MQh0eLR5A4JACH5BAUNAAUALAUAAAAVACAAAAPDWFqzBRC6yUZTwA6JefuYtkUZUA2B9oilZjJFkIbu5i4o+pDkvQQxkgAyEFgiDmBQwByOjpNAhBkQSDfOF0YKoMqugOtmEYZYrd8wd7zjNqtNJnIrbZqHdq2a272b5w9ffH4kDmpWPIkTYVSJPIsyiI5/hoKTEFJkMpiXaC9qnJebdHt7mxujD2iClkebQF2sg0SgsF6bk6k7oY5eWjQjPL6LGgQRBMGIxAMEzQDNULF6xdDQbBw0BDLIzchsxDIi4t8JADs=\"\nalt=\"An unused animation from TH01's MIKO.PTN\" style=\"height: 128px;\"\u003e\u003cimg\nsrc=\"data:image/gif;base64,R0lGODlhIAAgAPIFAKCgoPDw8AAAAPAAAKAAAP//AAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDQAFACwAAAAAIAAgAAAD1Fi63F7jyemGpfjFnZv4oKBY5NUVYTpC7Imm4WieIFDHrfvZQO9/Lo+gFwAUizbQqvMJOJ/OXlMEiWCaUGRxWp1Jdr6wVBAQWJzWCZiX/KGd5PTjtqtjB2X0NZUMvfNyXzB8f16CgzVGJYYefYhGZYsUZIggTyIlalGVWRuMC2RwiFlmJJqXpFkBkl9QkKhQA6WBHk+OHxZDqwCZp7cguYu0DKFjIbLCn8RRNshms6yTA7yzz8kUG2a8XQTI3aYnyeK5QQqlBOgDBE7fVDpnT+qx7g0JACH5BAkNAAUALAYAAAAZACAAAAPVWLrcNS6+QauFco3Aw/7UkxXCJgRn1VWSUHKptWbuZW8exrgCwPs1Wa7h8gGOyNqE4iH2AAGoFDgoqXSkU2d7rFmZOy1X6mpeFkWkunv6ghRpIzDpcTezvV+Rx6me4Tx7gSkoV2GDg1B+FoeIP4qGcECORTB/JCiUZX2RWpOIWzksmDCUW4t3YiihqiCjqlBbYh9faB2fQT0eAIxwHLg8VSW8vZ7AQYujpGyDFQJLRFHBBME30T4kFAR+270Nfg9f3RfPERCLBOkDBBzd5S1MK+wr7wwJACH5BAkNAAUALAMAAAAdACAAAAPqWLrcPC7CNYct0M6osg1DII6htnHaCIIme6GpKZca7M4463y3Bgg42iMkAA5+gKSy2JlRAgLRQJAMAKzW6dT4eRFBRZUqGdR1C2Fx9Rfl1pqKonK+bFfeDHmSyl6K3HhxckVUg2lQGCYOhIRsjCMCFC8NjJWNUomTlJaVVZGKi46cVH+SEW2jh58YpyKilWJui5CjsYELUZCxYlCaeWO7KlocaCOvRqRgJ5Suo1sAR8u/VsdFF0CRxLl7lhqrxMWOAwSEBN7gcVaRRgTk7eZT6IJxFu3j9hbZ8mj19gRS+fbxC0HiHwl9DRIAACH5BAUNAAQALAEAAAAaACAAAAPRSLrcPi7KNeq8lEB4hajVp4GgRA5fqmxaRKao563WE36AnMurDeICgHA4c5E0QUBAydwVKaVYYEqdCj0BASOKrFqZWC0t6hmaiYKseBCwQMrXazI3XR8JunxQRtWy7zJ6gX0jJQuBiDp1Gm2GeImIQlmFbnhOkEGLfzUoaphpmkedS59eH41ufZimFaijdaZekyeMVEpVoLZirqekiCG3OSuubZeBMUQUrgAxiRY8UFMnziEmbK4nLxO92FnSKB3XqN5pPxggbelU5uHZ6G1iBAkAOw==\"\nalt=\"An unused animation from TH01's MIKO.PTN\" style=\"height: 128px;\"\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWith that monster of a function taken care of, we've only got boss sprite animation as the final blocker of uninterrupted Sariel progress. Due to some unfavorable code layout in the Mima segment though, I'll need to spend a bit more time with some of the features used there. Next up: The missile bullets used in the Mima and YuugenMagan fights.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-11-08\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-10-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-10-20T09:58:26Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-10-09",
      "url": "https://rec98.nmlgc.net/blog/2021-10-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-10-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-09-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-10-09\"\u003e\u003ctime datetime=\"2021-10-09T21:29:21Z\"\u003e2021-10-09 21:29\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0160\"\u003eP0160\u003c/a\u003e\n\t\t\tTH01 decompilation (Pellet speed modification + HUD, part 3 (Stage timer) + Particle system)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e491cd7...42ba4a5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0161\"\u003eP0161\u003c/a\u003e\n\t\t\tResearch (Turbo C++ 4.0J's jump optimization bug after SCOPY@)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/42ba4a5...81dd96e\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/resident\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Data passed between the individual executables of each game.\"\u003eresident\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNothing really noteworthy in TH01's stage timer code, just yet another HUD\n\telement that is needlessly drawn into VRAM. Sure, ZUN applies his custom\n\tboldfacing effect on top of the glyphs retrieved from font ROM, but he could\n\thave easily installed those modified glyphs as gaiji.\u003cbr\u003e\n\tWell, OK, halfwidth gaiji aren't exactly well documented, and sometimes not\n\teven correctly emulated\n\t\u003ca href=\"/blog/2021-09-12\"\u003e📝 due to the same PC-98 hardware oddity I was researching last month\u003c/a\u003e.\n\tI've reserved two of the pending anonymous \"anything\" pushes for the\n\tconclusion of this research, just in case you were wondering why the\n\toutstanding workload is now lower after the two delivered here.\n\u003c/p\u003e\u003cp\u003e\n\tAnd since it doesn't seem to be clearly documented elsewhere: Every 2 ticks\n\ton the stage timer correspond to 4 frames.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, TH01 \u003cs\u003erank\u003c/s\u003e pellet speed. The resident pellet speed value is a\n\tfactor ranging from a minimum of -0.375 up to a maximum of 0.5 (pixels per\n\tframe), multiplied with the difficulty-adjusted base speed for each pellet\n\tand added on top of that same speed. This multiplier is modified\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eevery time the stage timer reaches 0 and\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e is shown (+0.05)\u003c/li\u003e\n\t\u003cli\u003efor every score-based extra life granted below the maximum number of\n\tlives (+0.025)\u003c/li\u003e\n\t\u003cli\u003eevery time a bomb is used (+0.025)\u003c/li\u003e\n\t\u003cli\u003eon every frame in which the \u003ccode\u003erand\u003c/code\u003e value (shown in debug\n\tmode) is evenly divisible by\n\t\u003ccode\u003e(1800 - (lives × 200) - (bombs × 50))\u003c/code\u003e (+0.025)\u003c/li\u003e\n\t\u003cli\u003eevery time Reimu got hit (set to 0 if higher, then -0.05)\u003c/li\u003e\n\t\u003cli\u003ewhen using a continue (set to -0.05 if higher, then -0.125)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tApparently, ZUN noted that these deltas couldn't be losslessly stored in an\n\tIEEE 754 floating-point variable, and therefore didn't store the pellet\n\tspeed factor exactly in a way that would correspond to its gameplay effect.\n\tInstead, it's stored similar to Q12.4 subpixels: as a simple integer,\n\tpre-multiplied by 40. This results in a raw range of -15 to 20, which is\n\twhat the undecompiled ASM calls still use. When spawning a new pellet, its\n\tbase speed is first multiplied by that factor, and then divided by 40 again.\n\tThis is actually quite smart: The calculation doesn't need to be aware of\n\teither Q12.4 \u003ci\u003eor\u003c/i\u003e the 40× format, as\n\t\u003ccode\u003e((Q12﻿.﻿4 * factor×40) / factor×40)\u003c/code\u003e still comes out as a\n\tQ12.4 subpixel even if all numbers are integers. The only limiting issue\n\there would be the potential overflow of the 16-bit multiplication at\n\tunadjusted base speeds of more than 50 pixels per frame, but that'd be\n\t\u003ci\u003eseriously\u003c/i\u003e unplayable.\u003cbr\u003e\n\tSo yeah, pellet speed modifications are indeed gradual, and don't just fall\n\tinto the coarse three \"high, normal, and low\" categories.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThat's ⅝ of P0160 done, and the continue and pause menus would make good\n\tcandidates to fill up the remaining ⅜… except that it seemed impossible to\n\tfigure out the correct compiler options for this code?\u003cbr\u003e\n\tThe issues centered around the two effects of Turbo C++ 4.0J's\n\t\u003ccode\u003e-O\u003c/code\u003e switch:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eOptimizing jump instructions: merging duplicate successive jumps into a\n\tsingle one, and merging duplicated instructions at the end of conditional\n\tbranches into a single place under a single branch, which the other branches\n\tthen jump to\u003c/li\u003e\n\t\u003cli\u003eCompressing \u003ccode\u003eADD SP\u003c/code\u003e and \u003ccode\u003ePOP CX\u003c/code\u003e\n\tstack-clearing instructions after multiple successive \u003ccode\u003eCALL\u003c/code\u003es to\n\t\u003ccode\u003e__cdecl\u003c/code\u003e functions into a single \u003ccode\u003eADD SP\u003c/code\u003e with the\n\tcombined parameter stack size of all function calls\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tBut how can the ASM for these functions exhibit #1 but \u003ci\u003enot\u003c/i\u003e #2? How\n\tcan it be seemingly optimized \u003ci\u003eand\u003c/i\u003e unoptimized at the same time? The\n\tonly option that gets somewhat close would be \u003ccode\u003e-O- -y\u003c/code\u003e, which\n\temits line number information into the .OBJ files for debugging. This\n\tcombination provides its own kind of #1, but these functions clearly need\n\tthe real deal.\n\u003c/p\u003e\u003cp\u003e\n\tThe research into this issue ended up consuming a full push on its own.\n\tIn the end, this solution turned out to be completely unrelated to compiler\n\toptions, and instead came from the effects of a compiler bug in a totally\n\tdifferent place. Initializing a local structure instance or array like\n\u003c/p\u003e\u003cpre\u003econst uint4_t flash_colors[3] = { 3, 4, 5 };\u003c/pre\u003e\u003cp\u003e\n\talways emits the \u003ccode\u003e{ 3, 4, 5 }\u003c/code\u003e array into the program's data\n\tsegment, and then generates a call to the internal \u003ccode\u003eSCOPY@\u003c/code\u003e\n\tfunction which copies this data array to the local variable on the stack.\n\tAnd as soon as this \u003ccode\u003eSCOPY@\u003c/code\u003e call is emitted, the \u003ccode\u003e-O\u003c/code\u003e\n\toptimization #1 is disabled \u003ci\u003efor the entire rest of the translation\n\tunit\u003c/i\u003e?!\u003cbr\u003e\n\tSo, any code segment with an \u003ccode\u003eSCOPY@\u003c/code\u003e call followed by\n\t\u003ccode\u003e__cdecl\u003c/code\u003e functions must strictly be decompiled from top to\n\tbottom, mirroring the original layout of translation units. That means no\n\tTH01 continue and pause menus before we haven't decompiled the bomb\n\tanimation, which contains such an \u003ccode\u003eSCOPY@\u003c/code\u003e call. 😕\u003cbr\u003e\n\tLuckily, TH01 is the only game where this bug leads to significant\n\trestrictions in decompilation order, as later games predominantly use the\n\t\u003ccode\u003epascal\u003c/code\u003e calling convention, in which each function itself clears\n\tits stack as part of its \u003ccode\u003eRET\u003c/code\u003e instruction.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhat now, then? With 51% of \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e decompiled, we're\n\tslowly running out of small features that can be decompiled within ⅜ of a\n\tpush. Good that I haven't been looking a lot into \u003ccode\u003eOP.EXE\u003c/code\u003e and\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e, which pretty much \u003ci\u003eonly\u003c/i\u003e got easy pieces of\n\tcode left to do. Maybe I'll end up finishing their decompilations entirely\n\twithin these smaller gaps?\u003cbr\u003e I still ended up finding one more small\n\tpiece in \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e though: The particle system, seen in the\n\tMima fight.\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2021-10-09-TH01-Particle-bug.webp?f109e3fb\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"56.423132\" data-frame-count=\"320\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2021-10-09-TH01-Particle-bug.avi?5457d34b\"\u003e\u003csource src=\"/blog/static/video/av1/2021-10-09-TH01-Particle-bug.webm?99c1825a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2021-10-09-TH01-Particle-bug.webm?813fda62\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2021-10-09-TH01-Particle-bug.webm?5698104a\" type=\"video/webm\"\u003eVideo of the particle system seen in the TH01 Mima fight that demonstrates its off-by-one bug. \u003ca href=\"/blog/static/video/zmbv/2021-10-09-TH01-Particle-bug.avi?5457d34b\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tI like how everything about this animation is contained within a single\n\tfunction that is called once per frame, but ZUN could have really\n\tconsolidated the spawning code for new particles a bit. In Mima's fight,\n\tparticles are only spawned from the top and right edges of the screen, but\n\tthe function in fact contains unused code for all other 7 possible\n\tdirections, written in quite a bloated manner. This wouldn't feel quite as\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e if ZUN had used an angle parameter instead…\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e Also, why unnecessarily waste another 40 bytes of\n\tthe BSS segment?\n\u003c/p\u003e\u003cp\u003e\n\tBut wait, what's going on with the very first spawned particle that just\n\tstops near the bottom edge of the screen in the video above? Well, even in\n\tsuch a simple and self-contained function, ZUN managed to include an\n\toff-by-one error. This one then results in an out-of-bounds array access on\n\tthe 80th frame, where the code attempts to spawn a 41\u003csup\u003est\u003c/sup\u003e\n\tparticle. If the first particle was unlucky to be both slow enough and\n\tspawned away far enough from the bottom and right edges, the spawning code\n\twill then kill it off before its unblitting code gets to run, leaving its\n\tpixel on the screen until something else overlaps it and causes it to be\n\tunblitted.\u003cbr\u003e\n\tWhich, during regular gameplay, will quickly happen with the Orb, all the\n\tpellets flying around, and your own player movement. Also, the RNG can\n\teasily spawn this particle at a position and velocity that causes it to\n\tleave the screen more quickly. Kind of impressive how ZUN laid out the\n\t\u003ca href=\"https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_Arrays\"\u003estructure\n\tof arrays\u003c/a\u003e in a way that ensured practically no effect of this bug on the\n\tgame; this glitch could have easily happened \u003ci\u003eevery\u003c/i\u003e 80 frames instead.\n\tHe \u003ci\u003ealmost\u003c/i\u003e got close to all bugs canceling out each other here!\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The player control functions, including the second-biggest function\n\tin all of PC-98 Touhou.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-10-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-09-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-10-09T21:29:21Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-09-28",
      "url": "https://rec98.nmlgc.net/blog/2021-09-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-10-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-09-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-09-28\"\u003e\u003ctime datetime=\"2021-09-28T16:05:58Z\"\u003e2021-09-28 16:05\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0158\"\u003eP0158\u003c/a\u003e\n\t\t\tTH01 decompilation (Items, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/bf7bb7e...c0c0ebc\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0159\"\u003eP0159\u003c/a\u003e\n\t\t\tTH01 decompilation (Items, part 2/2 + Cards)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c0c0ebc...e491cd7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/card-flipping\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tOf course, Sariel's potentially bloated and copy-pasted code is blocked by\n\teven more definitely bloated and copy-pasted code. It's TH01, what did you\n\texpect? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tBut even then, TH01's item code is on a new level of software architecture\n\tridiculousness. First, ZUN uses distinct arrays for both types of items,\n\twith their own caps of 4 for bomb items, and 10 for point items. Since that\n\tobviously makes any type-related \u003ccode\u003eswitch\u003c/code\u003e statement redundant,\n\the also used distinct \u003ci\u003efunctions\u003c/i\u003e for both types, with copy-pasted\n\tboilerplate code. The main per-item update and render function \u003ci\u003eis\u003c/i\u003e\n\tshared though… and takes \u003ci\u003eevery single accessed member of the item\n\tstructure as its own reference parameter\u003c/i\u003e. Like, why, you have a\n\tstructure, right there?! That's one way to really practice the C++ language\n\tconcept of passing arbitrary structure fields by mutable reference…\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tTo complete the unwarranted grand generic design of this function, it calls\n\tback into per-type collision detection, drop, and collect functions with\n\tanother three reference parameters. Yeah, why use C++ virtual methods when\n\tyou can also implement the effectively same polymorphism functionality by\n\thand? Oh, and the coordinate clamping code in one of these callbacks could\n\tonly possibly have come from nested \u003ccode\u003emin()\u003c/code\u003e and\n\t\u003ccode\u003emax()\u003c/code\u003e preprocessor macros. And that's how you extend such\n\tdead-simple functionality to 1¼ pushes…\n\u003c/p\u003e\u003cp\u003e\n\tAmidst all this jank, we've at least got a sensible item↔player hitbox this\n\ttime, with 24 pixels around Reimu's center point to the left and right, and\n\textending from 24 pixels above Reimu down to the bottom of the playfield.\n\tIt absolutely didn't look like that from the initial naive decompilation\n\tthough. Changing entity coordinates from left/top to center was one of the\n\tbetter lessons from TH01 that ZUN implemented in later games, it really\n\tmakes collision detection code much more intuitive to grasp.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe card flip code is where we find out some slightly more interesting\n\taspects about item drops in this game, and how they're controlled by a\n\thidden cycle variable:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAt the beginning of every 5-stage scene, this variable is set to a\n\trandom value in the [0..59] range\u003c/li\u003e\n\t\u003cli\u003ePoint items are dropped at every multiple of 10\u003c/li\u003e\n\t\u003cli\u003eEvery card flip adds 1 to its value after this \u003ccode\u003emod 10\u003c/code\u003e\n\tcheck\u003c/li\u003e\n\t\u003cli\u003eAt a value of 140, the point item is replaced with a bomb item, but only\n\tif no damaging bomb is active. In any case, its value is then reset to\n\t1.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t\u003cs\u003eThen again, score players largely ignore point items anyway, as card\n\tcombos simply have a much bigger effect on the score. With this, I should\n\thave RE'd all information necessary to construct a tool-assisted score run,\n\tthough?\u003c/s\u003e\u003cbr\u003e\n\t\u003cstrong\u003eEdit:\u003c/strong\u003e Turns out that 1) point items are becoming\n\tincreasingly important in score runs, and 2) Pearl already did a TAS some\n\tmonths ago. Thanks to\n\t\u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e for the info!\n\t\u003cscript\u003e\n\t\texternalRegister('2021-09-28', 'vid', 'https://youtube.com/embed/IJrYfHTaNCE');\n\t\u003c/script\u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ciframe id=\"2021-09-28-vid\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThe Orb↔card hitbox also makes perfect sense, with 24 pixels around\n\tthe center point of a card in every direction.\n\u003c/p\u003e\u003cp\u003e\n\tThe rest of the code confirms the\n\t\u003ca href=\"https://en.touhouwiki.net/wiki/Highly_Responsive_to_Prayers/Gameplay#Cards\"\u003ecard\n\tflip score formula documented on Touhou Wiki\u003c/a\u003e, as well as the way cards\n\tare flipped by bombs: During every of the 90 \"damaging\" frames of the\n\t140-frame bomb animation, there is a 75% chance to flip the card at the\n\t\u003ccode\u003e[bomb_frame % total_card_count_in_stage]\u003c/code\u003e array index. Since\n\tstages can only have up to 50 cards\n\t\u003ca href=\"/blog/2020-11-30\"\u003e📝 thanks to a bug\u003c/a\u003e, even a 75% chance is high\n\tenough to typically flip most cards during a bomb. Each of these flips\n\tstill only removes a single card HP, just like after a regular collision\n\twith the Orb.\u003cbr\u003e\n\tAlso, why are the card score popups rendered \u003ci\u003ebefore\u003c/i\u003e the cards\n\tthemselves? That's two needless frames of flicker during that 25-frame\n\tanimation. Not all too noticeable, but still.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's over 50% of \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e decompiled as well! Next\n\tup: More HUD update and rendering code… with a direct dependency on\n\t\u003cs\u003erank\u003c/s\u003e pellet speed modifications?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-10-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-09-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-09-28T16:05:58Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-09-12",
      "url": "https://rec98.nmlgc.net/blog/2021-09-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-09-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-08-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-09-12\"\u003e\u003ctime datetime=\"2021-09-12T18:33:09Z\"\u003e2021-09-12 18:33\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0157\"\u003eP0157\u003c/a\u003e\n\t\t\tTH01 decompilation (16× TRAM letters: 東方★靈異伝, STAGE #, and HARRY UP)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4bc6405...bf7bb7e\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tYup, there still are features that can be fully covered in a single push\n\tand don't lead to sprawling blog posts. The giant\n\t\u003cspan style=\"color: red\"\u003eSTAGE number\u003c/span\u003e and\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e messages, as well as the\n\tflashing transparent 東方★靈異伝 at the beginning of each scene are drawn\n\tby retrieving the glyphs for each letter from font ROM, and then \"blitting\"\n\tthem to text RAM by placing a colored fullwidth 16×16 square at every pixel\n\tthat is set in the font bitmap.\u003cbr\u003e\n\tAnd \u003ca href=\"/blog/2020-05-31\"\u003e📝 once again\u003c/a\u003e, ZUN's code there matches\n\tthe mediocre example code for the related hardware interrupt from the\n\t\u003ci\u003ePC-9801 Programmers' Bible\u003c/i\u003e. It's not 100% copied this time, but\n\tdefinitely inspired by the code on page 121. Therefore, we can conclude\n\tthat these letters are probably only displayed as these 16× scaled glyphs\n\tbecause that book had code on how to achieve this effect.\n\u003c/p\u003e\u003cp\u003e\n\tZUN \"improved\" on the example code by implementing a write-only cursor over\n\tthe entire text RAM that fills every 16×16 cell with a differently colored\n\tspace character, fully clearing the text RAM as a side effect. For once, he\n\teven removed some redundancy here by using helper functions! It's all still\n\tfar from \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/good-code\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/a\u003e\u003c/span\u003e though. For example, there's a\n\tfunction for filling 5 rows worth of cells, which he uses for both the top\n\tand bottom margin of these letters. But since the bottom margin starts at\n\tthe 22nd line, the code writes past the 25th line and into the second TRAM\n\tpage. Good that this page is not used by either the hardware or the game.\n\u003c/p\u003e\u003cp\u003e\n\tThese cursor functions can actually write any fullwidth JIS code point to\n\ttext RAM… and seem to do that in a rather simplified way, because shouldn't\n\tyou set the most significant bit to indicate the right half of a fullwidth\n\tcharacter? That's what's written in the same book that ZUN copied all\n\tfunctions out of, after all. 🤔 Researching this led me down quite the\n\trabbit hole, where I found an oddity in PC-98 text RAM rendering that no\n\tsingle one of the widely-used PC-98 emulators gets completely right. I'm\n\t\u003ci\u003ealmost\u003c/i\u003e done with the 2-push research into this issue, which will\n\tinclude fixes for DOSBox-X and Neko Project II. The only thing I'm missing\n\tto get these fully accurate is a screenshot of the output created by this binary, on any PC-98 model made by EPSON:\n\t\u003ca class=\"download\" href=\"/blog/static/2021-09-12-jist0x28.com.zip?80ccd025\" data-kb=\"3.4\"\u003e2021-09-12-jist0x28.com.zip \u003c/a\u003e\n\tThat's the reason why this push was rather delayed. Thanks in advance to\n\tanyone\twho'd like to help with this!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tIn maybe more disappointing news: Sariel is going to be delayed for a while\n\tlonger. 😕 The player- and HUD-related functions, which previously delayed\n\tfurther progress there, turned out to call a lot of not yet RE'd functions\n\tthemselves. Seems as if we're doing most of the\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/card-flipping\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/a\u003e\u003c/span\u003e code second, after all? Next up: Point and bomb items, which at least are a significant step in terms of position\n\tindependence.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-09-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-08-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-09-12T18:33:09Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-08-23",
      "url": "https://rec98.nmlgc.net/blog/2021-08-23",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-09-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-07-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-08-23\"\u003e\u003ctime datetime=\"2021-08-23T23:43:07Z\"\u003e2021-08-23 23:43\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0153\"\u003eP0153\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 3/5.5: Patterns 2-4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/624e0cb...d05c9ba\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0154\"\u003eP0154\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 4/5.5: Patterns 5-8)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d05c9ba...031b526\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0155\"\u003eP0155\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 5/5.5: Patterns 9-12)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/031b526...9ad578e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0156\"\u003eP0156\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 5.5/5.5: Main function + Sariel entrance animation + HARRY UP pellets)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9ad578e...4bc6405\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t\u003ca href=\"/blog/2021-05-27\"\u003e📝 7 pushes to get Konngara done, according to my previous estimate?\u003c/a\u003e\n\tWell, how about being twice as fast, and getting the entire boss fight done\n\tin 3.5 pushes instead? So much copy-pasted code in there… without any\n\tflashy \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/unused\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/a\u003e\u003c/span\u003e content, apart from four calculations with an unclear purpose. And the three strings \u003ccode\u003e\"ANGEL\", \"OF\",\n\t\"DEATH\"\u003c/code\u003e, which were probably meant to be rendered using those giant\n\tupscaled font ROM glyphs that also display the\n\t\u003cspan style=\"color: red\"\u003eSTAGE #\u003c/span\u003e and\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e strings? Those three strings\n\tare also part of Sariel's code, though.\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2021-08-23-TH01-Konngara-four_homing_snakes.webp?aae7fc15\" preload=\"none\" controls loop width=\"640\" height=\"336\" data-fps=\"56.423132\" data-frame-count=\"426\" style=\"aspect-ratio: 640 / 336\" data-lossless=\"/blog/static/video/zmbv/2021-08-23-TH01-Konngara-four_homing_snakes.avi?98fc092e\"\u003e\u003csource src=\"/blog/static/video/av1/2021-08-23-TH01-Konngara-four_homing_snakes.webm?04d02271\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2021-08-23-TH01-Konngara-four_homing_snakes.webm?fd91abd3\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2021-08-23-TH01-Konngara-four_homing_snakes.webm?1a3b6282\" type=\"video/webm\"\u003eVideo of Konngara's \"four homing snakes\" pattern. \u003ca href=\"/blog/static/video/zmbv/2021-08-23-TH01-Konngara-four_homing_snakes.avi?98fc092e\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tOn to the remaining 11 patterns then! Konngara's homing snakes, shown in\n\tthe video above, are one of the more notorious parts of this battle. They\n\toccur in two patterns – one with two snakes and one with four – with\n\t\u003ci\u003eall\u003c/i\u003e of the spawn, aim, update, and render code copy-pasted between\n\tthe two. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Three gameplay-related discoveries\n\there:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe homing target is locked once the Y position of a snake's white head\n\tdiamond is below 300 pixels.\u003c/li\u003e\n\t\u003cli\u003eThat diamond is also the only one with collision detection…\u003c/li\u003e\n\t\u003cli\u003e…but comes with a gigantic 30×30 pixel hitbox, reduced to 30×20 while\n\tReimu is sliding. For comparison: Reimu's regular sprite is 32×32 pixels,\n\tincluding transparent areas. This time, there \u003ci\u003eis\u003c/i\u003e a clearly defined\n\thitbox around Reimu's center pixel that the single top-left pixel can\n\tcollide with. No imagination necessary, which people apparently\n\t\u003ca href=\"/blog/2021-07-31\"\u003e📝 still\u003c/a\u003e prefer over actually understanding an\n\talgorithm… Then again, this hitbox is \u003ci\u003estill\u003c/i\u003e not intuitive at all,\n\tbecause…\n\t\u003cfigure class=\"pixelated\"\u003e\u003cimg\n\t\tsrc=\"data:image/gif;base64,R0lGODlhCAAIAKEBAAAAAP8AAP8AAP8AACH5BAEKAAIALAAAAAAIAAgAAAIOjARwm8ntoJxqPreQOgUAOw==\"\n\t\tstyle=\"height: 64px;\"\n\t\talt=\"Snake head with collision pixel\"\u003e\n\t\u003c/figure\u003e… the exact collision pixel, marked in\n\t\u003cspan style=\"color: red\"\u003ered\u003c/span\u003e, is part of the diamond sprite's\n\ttransparent background \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tThis was followed by \u003ci\u003ereally\u003c/i\u003e weird aiming code for the \"sprayed\n\tpellets from cup\" pattern… which can only possibly have been done on\n\tpurpose, but is sort of mitigated by the spraying motion anyway.\u003cbr\u003e\n\tAfter a bunch of long \u003ccode\u003eif(…) {…} else\u0026nbsp;if(…) {…} else\u0026nbsp;if(…)\n\t{…}\u003c/code\u003e chains, which remain quite popular in \u003ci\u003ecertain\u003c/i\u003e corners of\n\tthe game dev scene to this day, we've got the three \u003ci\u003esword slash\u003c/i\u003e\n\tpatterns as the final notable ones. At first, it seemed as if ZUN just\n\timprovised those raw number constants involved in the pellet spawner's\n\tmovement calculations to describe \u003ci\u003esome\u003c/i\u003e sort of path that vaguely\n\tresembles the sword slash. But once I tried to express these numbers in\n\tterms of the slash animation's keyframes, it all worked out perfectly, and\n\tresulted in this:\n\u003c/p\u003e\u003cfigure\n\tid=\"2021-08-23-easter-egg-container\"\n\tclass=\"pixelated\"\n\tonmouseover=\"document.getElementById('2021-08-23-easter-egg').style.opacity = '0.5';\"\n\tonmouseout=\"document.getElementById('2021-08-23-easter-egg').style.opacity = '0';\"\n\u003e\u003ca href=\"/blog/static/2021-08-23-TH01-Konngara-slash-triangle.png?68566919\" style=\"grid-row: 1; grid-column: 1;\"\u003e\u003cimg\n\tstyle=\"margin: 0;\"\n\tsrc=\"/blog/static/2021-08-23-TH01-Konngara-slash-triangle.png?68566919\"\n\talt=\"Triangular path of the pellet spawner during Konngara's slash patterns\"\u003e\u003c/a\u003e\n\u003ca href=\"/blog/static/2021-08-23-TH01-Konngara-slash-triangle.png?68566919\" style=\"grid-row: 1; grid-column: 1;\"\u003e\u003cimg\n\tid=\"2021-08-23-easter-egg\"\n\tstyle=\"margin: 0; grid-row: 1; grid-column: 1; display: block; opacity: 0; transition: opacity 5s;\"\n\tsrc=\"data:image/gif;base64,R0lGODlhgAKQAaECAAAAAFevTv8AAP8AACH5BAEKAAIALAAAAACAApABAAL+lI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8YhMKpfMpvMJjUqn1Kr1is1qt9yu9wsOi8fksvmMTqvX7Lb7DY/L5/S6/Y7P6/f8vv8PGCg4SFhoeIiYqLjI2Oj4CBkpOUlZaXmJmam5ydnp+QkaKjpKWmp6ipqqusra6voKGys7S1tre4ubq7vL2+v7CxwsPExcbHyMnKy8zNzs/AwdLT1NXW19jZ2tvc3d7f0NHi4+Tl5ufo6err7O3u7+Dh8vP09fb3+Pn6+/z9/v/w8woMCBBAsaPIgwocKFDBs6fAgxosSJFCtavIgxo8b+jRw7evwIMqTIkSRLmjyJMqXKlSxbunwJk0iAmDRNBLhZM2eImzh1+tTAE4DQn0QrBBU6tKjSB0eRApi5NCqCpk57Sl3KM4DTqlCvFqW69WlXrzrBhhVL1qfZs1bTwsyqdWtcrm7fwp2LFOzYuirv3gyrdy/fkn7XshU8WGRWoXfPAkac2CNcrosd54UcWePkx5sdt83MsbJlv54xg6Yo2vLlzpdPazasWizrz64nwo69+u/q2qh1yy2M+zBvib45Aw/eerjD27ILF49tWrlA5rmd4x0tXSF149ZVR8/Ob7t368/zgi8oHnl3wOenl2dMGnnzxnTb90tNeT3u48n+7etjTh5rhwlIm3/z4DdggALmd12BBsKTnoL8qffdg+ik95uECI5WoYXkYDiehteV5uE77823YGkKBudgieOcCJ9+LAYIXYcubgNjjCtSKONvN5qznYgjctgjfD++GKGI8qEYn2xHhgMik0qGKOGT3mzIWI1CpliddVbiuOGEKm45pI6itfglNFxqyN2WRD6HZprL0JcgeXWSWSaJckrT5J3OtYmnfHHuWUyRDMpIp5k7vkloM4u+6SaghlLW6Jxz0TjjlBmyqWKlyOQ46aaPSjlqf54Ok6ekOSqKp5CPnYpqqpBy2SWZok7IE6y/RKkqlZEe2l2uuvJiGK2++sr+KatVDpsLlnYKamyrrTJ7C5a1/jkrqNL6FiC1tWDoKrDWkirhoaZ668q4S5KrqZblVncuuqt0xqu4246p1bPzxSvvKcCFCu22Q072q7D9+osth+v6aSu8yiZs8MGkmLUquxULrO/DFA8qMSawyaqxmEyqiumtYXLccSW3rYWxoYEyPDDKKUeirsUgw4zgyybnLPPMjdSs48I8Ntknu9cWHbHPltQr9JLtHp0xgTYqjQjQTV+9c6JQF50bpVRLYjXWYmeN36+b7vY1JEyPLXbLCWeIdtqM5Mx23U4/TSpnccudyIlrZrp20xnbS3TPfOexsrPauh22xviKbPjhdWj+LfTJo3K98OAhCyf5ICJnPjjkxgaMqMtTdx4Hy4m/Z7rZVzPOOep8NC4qw49LW3u2+i4YuexpUG530Lcz/jbhrM92uu9nFB/8fsQD7TfwXcaovB2NjV4x4OGGnOTN2TpZvRxaj0v+xa7bvC7IidLXe/hejK5liKA/T/vwDqPlfhvwN982/dIjyy2qJC9/Wagf/+z2rvPZbzwEREP2+Le9+fFHgfhyXgPLsDoDbq1VQ9sM3oLXvgs+IYMPxJkAcbU/4zHvgCEU4RLUVcIKosh2wCIc2T7mvQH5yIVcSF8OZag9D+JOcABLUH14mIXKycpyJ/RffH6owhSmZoBILEL+DDeINCdGUIk606F5qkgFXmlxjJ9T1PAwpbq4JA2MTegeGd8osBuWD1BsHCGItvg6OJZsY5gj3RrreIT6ffCAcrwXZQh5GX4BcgjQ42CMEAlAQ7KNOk1p4SJjgL1kVQWSY4ujDwPIxyle0orR0hwnISlJ5x1FenoZpRBWaLRTytKERXRYChnjSiDQ63h9ZCEUQdjFMXUylzxIUSgDl75ZqnKX/7vb+IiZA1rxTINKFKYy4UNDLq6yQdC8AQyn+UusRW+JsCRikRpUqi92UwbSFN0Vxbkxd1FQd+O7VDpxuU4YlJJ51BSL3vLzGHlq8pNRK5138vkC+JmSaYFBZ2D+BFpObW6zOO9kD0JXIMWFVhSbAGWQXCDaS3jCiaI+lMtFVUC7dG60kmVrKUhvOT+4cbSjXVPkST2QRsVNSpBiqudLkUklcZmLKzu86U5gFNGdNvNOtyJqJJfKlqE1laYMNCoISAhVM5LNjyZzal62KjjSnQ2eVsVpkGq5GloCrqtezRJbw+pMvATOkmUVQOPQ+tT9TZFr6DQOPCV4yKhasK4YYCI9f1gy4ckPXvFMJATzKNfjsYiwFtAp5WDpUM19k6ICDGhaEZgpLz5ScJSlQPn42s5gNrOJcPpNYDt5M8u11YelhYDV3Jmqd9lsTaxtEAB+C9yJbvS3D7wsZ4H+i9zkKje4tXXAcIuFw8TutqV7rRnBoiQeUCr3hNu1FhXzCdTlErdlyCVeebPa3UwWl7Mj4u55ndXcBBgzouId6HEwJt6R5Te/LDVlfRua3vO+N7fxPQBvpbtf9A04gQNd8B3bG9LlSq087h1vgr/rygi/7az6weN0mYlefyZ4xOltKIWPa+H9YniR9E0udTfrthJHsIwkrrGKJzzO8gqYv7X1741dTF+VOhHIIbbxiNe3WSO7mLANVrLFjrzCMTq4nzbuk15XWtcIl7jGCCYybvXYYifP0Ms5FKNVi+xgLvtYwbCbcsOc/Dk095fEN0UzmW/Lz1R+GLr45e857Sz+4uDS+aIw9fOat3znO9czmIrWYqPFLJvtjhihV/6fbkkMyugi+dJj/q//ZIzdCeoHvFkN1T1B3cctLjqkbU60mrOIObo2UKFBPvSeIdzl8c5T144M7j4xC9UVgzF6EtZ0nPvcaO+GK7tT6uI4fynsUaoxT/OldU/9e2rd8rTVF87eiQrMAFzj2NtlPDWvl7rhxkJ6wetWMaKZC+4I9KiUefN0k89t7b8UrontvrH3YH2XeBe2t3vkNL6xbWtNvbk5/8W0Q7stcLN+8J6D9DVad3e9jEf2mwH2coIjTgJzt87DqC4hp1Jb5BenCuQozbWUbh3mekMZjoZGHsWAy/L+hCoVjfvWsoLFDOYTbxO5Oa9BpY/J6Jr7vN8WN3G3zVL0HcScflW+N9CN2++oB0HleoRz0Cl87hprPZBd3p65+53K/o4UL2NvY7UfpWemq7njPY9L26egG55rcohyr/qicX53LJiXXAdHdt8nHfgulO5pb1R0scWe+DFcV4jrAV6Mm25za0VeDexjNBPVHrNV73XzcBiyq438dlGSfnKiF67L+16Y1c9u2ryLVMyPLHtBgDOzIMZ6lXNftWvvftUi9hLw59Y61NvzOMefRJP14uL9Nj8TSU/xcqf/CU5jDvul+Ff5uD+vCIM/FlYef7XgYv70q3/97G+/+98P//j+y3/+9K+//e+P//zrf//877///w+AASiAA0iABWiAB4iACaiAC8iADeiADwiBESiBE0iBFWiBF4iBGaiBG8iBHeiBHwiCISiCI0iCJWiCJ4iCKaiCK8iCLeiCLwiDMSiDM0iDNWiDN4iDOaiDO8iDPeiDPwiEQSiEQ0iERWiER4iESaiES8iETeiETwiFUSiFU0iFVWiFV4iFWaiFW8iFXeiFXwiGYSiGY0iGZWiGZ4iGaaiGa8iGbeiGbwiHcSiHc0iHdWiHd4iHeaiHe8iHfeiHfwiIgSiIg0iIhWiIh4iIiaiIi8iIjeiIjwiJkSiJk0iJlWiJl4iJmaiJm8gTiZ3oiZ8IiqEoiqNIiqVoijNTAAA7\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tYup, the spawner always takes an exact path along this triangle. Sometimes,\n\tI wonder whether I should just rush this project and don't bother about\n\tnaming these repeated number literals. Then I gain insights like these, and\n\tit's all worth it.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tFinally, we've got Konngara's main function, which coordinates the entire\n\tfight. Third-longest function in both TH01 and all of PC-98 Touhou, only\n\tbehind some player-related stuff and YuugenMagan's gigantic main function…\n\tand it's even more of a copy-pasta, making it feel not nearly as long as it\n\tis. Key insights there:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe fight consists of 7 phases, with the entire defeat sequence being\n\tpart of the \u003ccode\u003eif(boss_phase\u0026nbsp;==\u0026nbsp;7) {…}\u003c/code\u003e\n\tbranch.\u003c/li\u003e\n\t\u003cli\u003eThe three even-numbered phases, however, only light up the Siddhaṃ seed\n\tsyllables and then progress to the next phase.\u003c/li\u003e\n\t\u003cli\u003eOdd-numbered phases are completed after passing an HP threshold or after\n\tseeing a predetermined number of patterns, whatever happens first. No\n\tpossibility of skipping anything there.\u003c/li\u003e\n\t\u003cli\u003ePatterns are chosen randomly, but the available \u003ci\u003epool\u003c/i\u003e of patterns\n\tis limited to 3 specific \"easier\" patterns in phases 1 and 5, and 4 patterns\n\tin phase 3. Once Phase 7 is reached at 9 HP remaining, all 12 patterns can\n\tpotentially appear. Fittingly, that's also the point where the red section\n\tof the HP bar starts.\u003cul\u003e\n\t\t\u003cli\u003eEvery time a pattern is chosen, the code only makes a maximum of two\n\t\tattempts at picking a pattern that's different from the one that\n\t\tKonngara just completed. Therefore, it seems entirely possible to see\n\t\tthe same pattern twice. Calculating an actual seed to prove that is out\n\t\tof the scope of this project, though.\u003c/li\u003e\n\t\t\u003cli\u003eDue to what looks like a copy-paste mistake, the pool for the second\n\t\tRNG attempt in phases 5 and 7 is reduced to only the first two patterns\n\t\tof the respective phases? That's already quite some bias right there,\n\t\tand we haven't even analyzed the RNG in detail yet…\n\t\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e (For anyone interested, it's a\n\t\t\u003ca href=\"https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use\"\u003eLCG,\n\t\tusing the \u003ci\u003eBorland C/C++\u003c/i\u003e parameters as shown here\u003c/a\u003e.)\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eThe difficulty level only affects the speed and firing intervals (and\n\tthus, number) of pellets, as well as the number of lasers in the one pattern\n\tthat uses them.\u003c/li\u003e\n\t\u003cli\u003eAfter the \u003ca href=\"/blog/2020-03-07\"\u003e📝 kuji-in defeat sequence\u003c/a\u003e, the\n\tfight ends in an attempted double-\u003ccode\u003efree\u003c/code\u003e of Konngara's image\n\tdata. \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e Thankfully, the format-specific\n\t\u003ccode\u003e_free()\u003c/code\u003e functions defend against such a thing.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tSeriously, \u003ca href=\"/blog/2020-01-14\"\u003e📝 line drawing\u003c/a\u003e was much harder to\n\tdecompile.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's it for Konngara! First boss with not a single piece of ASM left,\n\t30 more to go! 🎉 But wait, what about the cause behind the temporary green\n\tdiscoloration after leaving the Pause menu? I expected to find something on\n\tthat as well, but nope, it's nothing in Konngara's code segment. We'll\n\tprobably only get to figure that out near the very end of TH01's\n\tdecompilation, once we get to the one function that directly calls all of\n\tthe boss-specific main functions in a \u003ccode\u003eswitch\u003c/code\u003e statement.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2022-07-17):\u003c/strong\u003e\n\t\u003ca href=\"/blog/2022-07-17\"\u003e📝 Only took until Mima.\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo, Sariel next? With half of a push left, I did cover Sariel's first few\n\tinitialization functions, but all the sprite unblitting and HUD\n\tmanipulation will need some extra attention first. The first one of these\n\tfunctions is related to the HUD, the stage timer, and the\n\t\u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e mode, whose pellet pattern I've\n\talso decompiled now.\n\u003c/p\u003e\u003cp\u003e\n\tAll of this brings us past 75% PI in all games, and TH01 to under 30,000\n\tremaining ASM instructions, leaving TH03 as the now most expensive game to\n\tbe completely decompiled. Looking forward to how much more TH01's code will\n\tfall apart if you just tap it lightly… Next up: The aforementioned helper\n\tfunctions related to \u003cspan style=\"color: red\"\u003eHARRY UP\u003c/span\u003e, drawing the\n\tHUD, and unblitting the other bosses whose sprites are a bit more animated.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-09-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-07-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-08-23T23:43:07Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-07-31",
      "url": "https://rec98.nmlgc.net/blog/2021-07-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-08-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-07-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-07-31\"\u003e\u003ctime datetime=\"2021-07-31T19:19:27Z\"\u003e2021-07-31 19:19\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0149\"\u003eP0149\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Incompletely or wrongly RE'd bullet mechanics)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e1a26bb...05e4c4a\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0150\"\u003eP0150\u003c/a\u003e\n\t\t\tTH04 decompilation (Bullet spawning)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/05e4c4a...768251d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0151\"\u003eP0151\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Bullet updates)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/768251d...4d24ca5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0152\"\u003eP0152\u003c/a\u003e\n\t\t\tTH05 RE (Undecompilable bullet spawning code)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4d24ca5...81fc861\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, Ember2528, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t…or maybe not \u003ci\u003ethat\u003c/i\u003e soon, as it would have only wasted time to\n\tuntangle the bullet update commits from the rest of the progress. So,\n\there's \u003ci\u003eall\u003c/i\u003e the bullet spawning code in TH04 and TH05 instead. I hope\n\tyou're ready for this, there's a lot to talk about!\n\u003c/p\u003e\u003cp\u003e\n\t(For the sake of readability, \"bullets\" in this blog post refers to the\n\twhite 8×8 pellets\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAgMAAAC5YVYYAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAAlQTFRFAAAAqqr/////38h8nAAAAAF0Uk5TAEDm2GYAAAAeSURBVAjXY+BawKC1gmHVKoZVK0EoaymDaAgDawAAXhcHRDMdG+8AAAAASUVORK5CYII=\"\u003e\n\tand all 16×16 bullets loaded from \u003ccode\u003eMIKO16.BFT\u003c/code\u003e, nothing else.)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tBut first, what was going \u003ci\u003eon\u003c/i\u003e\n\t\u003ca href=\"/blog/2020-02-16\"\u003e📝 in 2020\u003c/a\u003e? Spent 4 pushes on the basic types\n\tand constants back then, still ended up confusing a couple of things, and\n\teven getting some wrong. Like how TH05's \"bullet slowdown\" flag actually\n\talways \u003ci\u003eprevents\u003c/i\u003e slowdown and fires bullets at a constant speed\n\tinstead. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Or how \"random spread\" is not the\n\tbest term to describe that unused bullet group type in TH04.\u003cbr\u003e\n\tOr that there are two distinct ways of clearing all bullets on screen,\n\twhich deserve different names:\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2021-07-31-TH04-Bullet-clear.webp?8bc15636\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"15\" data-frame-count=\"100\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2021-07-31-TH04-Bullet-clear.avi?d3a1a1ae\"\u003e\u003csource src=\"/blog/static/video/av1/2021-07-31-TH04-Bullet-clear.webm?fcb4e271\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2021-07-31-TH04-Bullet-clear.webm?ca2f5b81\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2021-07-31-TH04-Bullet-clear.webm?75da8ddd\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2021-07-31-TH04-Bullet-clear.avi?d3a1a1ae\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eMechanic #1: \u003ci\u003eClearing\u003c/i\u003e bullets for a custom amount of\n\t\ttime, awarding 1000 points for all bullets alive on the first frame,\n\t\tand 100 points for all bullets spawned during the clear time.\n\t\t\u003c/figcaption\u003e\n\t\u003c/figure\u003e\u003cfigure style=\"width: 640px\"\u003e\n\t\t\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2021-07-31-TH04-Bullet-zap.webp?6273b154\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"15\" data-frame-count=\"60\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2021-07-31-TH04-Bullet-zap.avi?89c5ad82\"\u003e\u003csource src=\"/blog/static/video/av1/2021-07-31-TH04-Bullet-zap.webm?44b55d85\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2021-07-31-TH04-Bullet-zap.webm?3fb46a3e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2021-07-31-TH04-Bullet-zap.webm?87254768\" type=\"video/webm\"\u003e\u003ca href=\"/blog/static/video/zmbv/2021-07-31-TH04-Bullet-zap.avi?89c5ad82\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\n\t\t\u003cfigcaption\u003eMechanic #2: \u003ci\u003eZapping\u003c/i\u003e bullets for a fixed 16 frames,\n\t\tawarding a semi-exponential and loudly announced \u003ci\u003eBonus!!\u003c/i\u003e for all\n\t\tbullets alive on the first frame, and preventing new bullets from being\n\t\tspawned during those 16 frames. In TH04 at least; thanks to a ZUN bug,\n\t\tzapping got reduced to 1 frame and no animation in TH05…\u003c/figcaption\u003e\n\t\u003c/figure\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tBullets are \u003ci\u003ezapped\u003c/i\u003e at the end of most midboss and boss phases, and\n\t\u003ci\u003ecleared\u003c/i\u003e everywhere else – most notably, during bombs, when losing a\n\tlife, or as rewards for extends or a maximized Dream bonus. The \u003ci\u003e\n\tBonus!!\u003c/i\u003e points awarded for zapping bullets are calculated iteratively,\n\tso it's not trivial to give an exact formula for these. For a small number\n\t𝑛 of bullets, it would exactly be 5𝑛³\u0026nbsp;-\u0026nbsp;10𝑛²\u0026nbsp;+\u0026nbsp;15𝑛\n\tpoints – or, using uth05win's (correct) recursive definition, \u003ccode\u003e\n\tBonus(𝑛) = Bonus(𝑛-1)\u0026nbsp;+\u0026nbsp;15𝑛²\u0026nbsp;-\u0026nbsp;5𝑛\u0026nbsp;+\u0026nbsp;10\u003c/code\u003e.\n\tHowever, one of the internal step variables is capped at a different number\n\tof points for each difficulty (and game), after which the points only\n\tincrease linearly. Hence, \"semi-exponential\".\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOn to TH04's bullet spawn code then, because that one can at least be\n\tdecompiled. And immediately, we have to deal with a pointless distinction\n\tbetween \u003ci\u003eregular\u003c/i\u003e bullets, with either a decelerating or constant\n\tvelocity, and \u003ci\u003especial\u003c/i\u003e bullets, with preset velocity changes during\n\ttheir lifetime. That preset has to be set \u003ci\u003esomewhere\u003c/i\u003e, so why have\n\tseparate functions? In TH04, this separation continues even down to the\n\tlowest level of functions, where values are written into the global bullet\n\tarray. TH05 merges those two functions into one, but then goes too far and\n\tuses self-modifying code to save a grand total of two local variables…\n\tLuckily, the rest of its actual code is identical to TH04.\n\u003c/p\u003e\u003cp\u003e\n\tMost of the complexity in bullet spawning comes from the (thankfully\n\tshared) helper function that calculates the velocities of the individual\n\tbullets within a group. Both games handle each group type via a large\n\t\u003ccode\u003eswitch\u003c/code\u003e statement, which is where TH04 shows off another Turbo\n\tC++ 4.0 optimization: If the range of \u003ccode\u003ecase\u003c/code\u003e values is too\n\tsparse to be meaningfully expressed in a jump table, it usually generates a\n\tlinear search through a second value table. But with the \u003ccode\u003e-G\u003c/code\u003e\n\tcommand-line option, it instead generates branching code for a binary\n\tsearch through the set of cases. 𝑂(log\u0026nbsp;𝑛) as the worst case for a\n\t\u003ccode\u003eswitch\u003c/code\u003e statement in a C++ compiler from 1994… that's so cool.\n\tBut still, why are the values in TH04's group type \u003ccode\u003eenum\u003c/code\u003e all\n\tover the place to begin with? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tUnfortunately, this optimization is pretty rare in PC-98 Touhou. It only\n\tshows up here and in a few places in TH02, compared to at least 50\n\t\u003ccode\u003eswitch\u003c/code\u003e value tables.\n\u003c/p\u003e\u003cp\u003e\n\tIn all of its micro-optimized pointlessness, TH05's undecompilable version\n\tat least fixes some of TH04's redundancy. While it's still not even\n\t\u003ci\u003eoptimal\u003c/i\u003e, it's at least a decently \u003ci\u003ewritten\u003c/i\u003e piece of ASM…\n\t\u003ci\u003eif\u003c/i\u003e you take the time to understand what's going on there, because it\n\tcertainly took quite a bit of that to verify that all of the things which\n\tlooked like bugs or quirks were in fact correct. And that's how the code\n\tfor this function ended up with 35% comments and blank lines before I could\n\tconfidently call it \"reverse-engineered\"…\u003cbr\u003e\n\tOh well, at least it finally fixes a correctness issue from TH01 and TH04,\n\twhere an invalid bullet group type would fill all remaining slots in the\n\tbullet array with identical versions of the first bullet.\n\u003c/p\u003e\u003cp\u003e\n\tSomething that both games also share in these functions is an over-reliance\n\ton globals for return values or other local state. The most ridiculous\n\texample here: Tuning the speed of a bullet based on rank actually mutates\n\tthe global bullet template… which ZUN then works around by adding a wrapper\n\tfunction around both regular and special bullet spawning, which saves the\n\tbase speed before executing that function, and restores it afterward.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Add another set of wrappers to bypass that exact\n\ttuning, and you've expanded your nice 1-function interface to 4 functions.\n\tOh, and did I mention that TH04 pointlessly duplicates the first set of\n\twrapper functions for 3 of the 4 difficulties, which can't even be\n\texplained with \"debugging reasons\"? That's 10 functions then… and probably\n\texplains why I've procrastinated this feature for so long.\n\u003c/p\u003e\u003cp\u003e\n\tAt this point, I also finally stopped decompiling ZUN's original ASM just\n\tfor the sake of it. All these small TH05 functions would look horribly\n\tunidiomatic, are identical to their decompiled TH04 counterparts anyway,\n\texcept for some unique constant… and, in the case of TH05's rank-based\n\tspeed tuning function, actually \u003ci\u003ebecome\u003c/i\u003e undecompilable as soon as we\n\twant to return a C++ class to preserve the semantic meaning of the return\n\tvalue. Mainly, this is because Turbo C++ does not allow register\n\tpseudo-variables like \u003ccode\u003e_AX\u003c/code\u003e or \u003ccode\u003e_AL\u003c/code\u003e to be cast into\n\tclass types, even if their size matches. Decompiling that function would\n\thave therefore lowered the quality of the rest of the decompiled code, in\n\texchange for the additional maintenance and compile-time cost of another\n\ttranslation unit. Not worth it – and for a TH05 port, you'd already have to\n\tdecompile all the rest of the bullet spawning code anyway!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe only thing in there that \u003ci\u003ewas\u003c/i\u003e still somewhat worth being\n\tdecompiled was the pre-spawn clipping and collision detection function. Due\n\tto what's probably a micro-optimization mistake, the TH05 version continues\n\tto spawn a bullet even if it was spawned on top of the player. This might\n\tsound like it has a different effect on gameplay… until you realize that\n\tthe player got hit in this case and will either lose a life or deathbomb,\n\tboth of which will cause all on-screen bullets to be \u003ci\u003ecleared\u003c/i\u003e anyway.\n\tSo it's at most a visual glitch.\n\u003c/p\u003e\u003cp\u003e\n\tBut while we're at it, can we please stop talking about hitboxes? At least\n\tin the context of TH04 and TH05 bullets. The actual collision detection is\n\tdescribed way better as a kill \u003ci\u003edelta\u003c/i\u003e of 8×8 pixels between the\n\tcenter points of the player and a bullet. You can distribute these pixels\n\tto any combination of bullet and player \"hitboxes\" that make up 8×8. 4×4\n\taround both the player and bullets? 1×1 for bullets, and 8×8 for the\n\tplayer? All equally valid… or perhaps none of them, once you keep in mind\n\tthat other entity types might have different kill deltas. With that in\n\tmind, the concept of a \"hitbox\" turns into just a confusing abstraction.\n\u003c/p\u003e\u003cp\u003e\n\tThe same is true for the 36×44 graze \u003cs\u003ebox\u003c/s\u003e delta. For some reason,\n\tthis one is not exactly  around the center of a bullet, but shifted to the\n\tright by 2 pixels. So, a bullet can be grazed up to 20 pixels right of the\n\tplayer, but only up to 16 pixels left of the player. uth05win also spotted\n\tthis… and rotated the deltas clockwise by 90°?!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWhich brings us to the bullet updates… for which I \u003ci\u003estill\u003c/i\u003e had to\n\tresearch a decompilation workaround, because\n\t\u003ca href=\"/blog/2021-07-20\"\u003e📝 P0148\u003c/a\u003e turned out to not help at all?\n\tInstead, the solution was to lie to the compiler about the true segment\n\tdistance of the popup function and declare its signature \u003ccode\u003efar\u003c/code\u003e\n\trather than \u003ccode\u003enear\u003c/code\u003e. This allowed ZUN to save that \u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"Hint: There is no difference in the amount of x86 opcode bytes.\"\u003e\n\t\u003ci\u003eridiculous\u003c/i\u003e overhead\u003c/span\u003e of 1 additional \u003ccode\u003efar\u003c/code\u003e function\n\tcall/return per frame, and those \u003ci\u003eprecious\u003c/i\u003e 2 bytes in the BSS segment\n\tthat he didn't have to spend on a segment value.\n\t\u003ca href=\"/blog/2020-05-04\"\u003e📝 Another function\u003c/a\u003e that didn't have just a\n\tsingle declaration in a common header file… really,\n\t\u003ca href=\"/blog/2021-01-06\"\u003e📝 how were these games even \u003ci\u003ebuilt\u003c/i\u003e???\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tThe function itself is among the longer ones in both games. It especially\n\tstands out in the indentation department, with 7 levels at its most\n\tindented point – and that's the \u003ci\u003eminimum\u003c/i\u003e of what's possible without\n\t\u003ccode\u003egoto\u003c/code\u003e. Only two more notable discoveries there:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eBullets are the only entity affected by Slow Mode. If the number of\n\tbullets on screen is ≥ \u003ccode\u003e(24 + (difficulty * 8) + rank)\u003c/code\u003e in TH04,\n\tor \u003ccode\u003e(42 + (difficulty * 8))\u003c/code\u003e in TH05, Slow Mode reduces the frame\n\trate by 33%, by waiting for one additional VSync event every two frames.\n\t\u003cbr\u003e\n\tThe code also reveals a second tier, with 50% slowdown for a slightly\n\thigher number of bullets, but that conditional branch can never be executed\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eBullets must have been grazed in a previous frame before they can\n\tbe collided with. (Note how this does not apply to bullets that spawned\n\ton top of the player, as explained earlier!)\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tWhew… When did ReC98 turn into a full-on code review?! 😅 And after all\n\tthis, we're \u003ci\u003estill\u003c/i\u003e not done with TH04 and TH05 bullets, with all the\n\tspecial movement types still missing. That should be less than one push\n\tthough, once we get to it. Next up: Back to TH01 and Konngara! Now have fun\n\trewriting the Touhou Wiki Gameplay pages 😛\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-08-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-07-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-07-31T19:19:27Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-07-20",
      "url": "https://rec98.nmlgc.net/blog/2021-07-20",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-07-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-07-20\"\u003e\u003ctime datetime=\"2021-07-20T22:35:20Z\"\u003e2021-07-20 22:35\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0148\"\u003eP0148\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Text popups, gather circle rendering, player position clamping)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c940059...e1a26bb\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gaiji\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBack after taking way too long to get \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e's MediaWiki\n\tupdate feature complete… I'm still waiting for more translators to test and\n\treview the new translation interface before delivering and deploying it\n\tall, which will most likely lead to another break from ReC98 within the\n\tnext few months. For now though, I'm happy to have mostly addressed the\n\tnagging responsibility I still had after willing that site into existence,\n\tand to be back working on ReC98. 🙂\n\u003c/p\u003e\u003cp\u003e\n\tAs announced, the next few pushes will focus on TH04's and TH05's bullet\n\tspawning code, before I get to put all that accumulated TH01 money towards\n\tfinishing all of \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/konngara\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/a\u003e\u003c/span\u003e's code in TH01. For a full\n\tpicture of what's happening with bullets, we'd \u003ci\u003ereally\u003c/i\u003e also like to\n\thave the bullet update function as readable C code though.\u003cbr\u003e\n\tClearing all bullets on the playfield will trigger a \u003ci\u003eBonus!!\u003c/i\u003e popup,\n\tdisplayed as \u003ca href=\"/blog/2020-09-16\"\u003e📝 gaiji\u003c/a\u003e in that proportional\n\tfont. Unfortunately, TLINK refused to link the code as soon as I referenced\n\tthe function for animating the popups at the top of the playfield? Which\n\tcan only mean that we have to decompile that function first…\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo, let's turn that piece of technical debt into a full push, and first\n\tdecompile another random set of previously reverse-engineered TH04 and TH05\n\tfunctions. Most of these are stored in a different place within the two\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e binaries, and the tried-and-true method of matching\n\tsegment names would therefore have introduced several unnecessary\n\ttranslation units. So I resorted to a segment splitting technique I should\n\thave started using way earlier: Simply creating new segments with names\n\tderived from their functions, at the exact positions they're needed. All\n\tthe new segment start and end directives do bloat the ASM code somewhat,\n\tand certainly contributed to this push barely removing any actual lines of\n\tcode. However, what we get in return is total freedom as far as\n\tdecompilation order is concerned,\n\t\u003ca href=\"/blog/2020-08-16\"\u003e📝 which should be the case for any ReC project, really\u003c/a\u003e.\n\tAnd in the end, all these tiny code segments will cancel out anyway.\u003cbr\u003e\n\tIf only we could do the same with the data segment…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe popup function happened to be the final one I RE'd before my long break\n\tin the spring of 2019. Back then, I didn't even bother looking into that\n\t64-frame delay between changing popups, and what that meant for the game.\n\t\u003cbr\u003e\n\tEach of these popups stays on screen for 128 frames, during which, of\n\tcourse, another popup-worthy event might happen. Handling this cleanly\n\twithout removing previous popups too early would involve some sort of event\n\tqueue, whose size might even be meaningfully limited to the number of\n\tdistinct events that can happen. But still, that'd be a data structure, and\n\twe're not gonna have \u003ci\u003ethat\u003c/i\u003e! \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Instead, ZUN\n\tsimply keeps two variables for the new and current popup ID. During an\n\tactive popup, any change to that ID will only be committed once the current\n\tpopup has been shown for at least 64 frames. And during \u003ci\u003ethat\u003c/i\u003e time,\n\tthat new ID can be freely overwritten with a different one, which drops any\n\tprevious, undisplayed event. But surely, there won't be more than two\n\tevents happening within 63 frames, right? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tThe rest was fairly uneventful – no newly RE'd functions in this push,\n\tafter all – until I reached the widely used helper function for applying\n\tthe current vertical scrolling offset to a Y coordinate. Its combination of\n\ta function parameter, the \u003ccode\u003epascal\u003c/code\u003e calling convention, and no\n\tstack frame was previously thought to be undecompilable… except that it\n\tisn't, and the decompilation didn't even require any new workarounds to be\n\tdeveloped? Good thing that I already forgot how impossible it was to\n\tdecompile the first function I looked at that fell into this category!\u003cbr\u003e\n\tOh well, this discovery wasn't \u003ci\u003etoo\u003c/i\u003e groundbreaking. Looking back at\n\tall the other functions with that combination only revealed a grand total\n\tof 1 additional one where a decompilation made sense: TH05's version of\n\t\u003ccode\u003esnd_kaja_interrupt()\u003c/code\u003e, which is now compiled from the same C++\n\tfile for all 4 games that use it. And well, looks like some quirks really\n\tremain unnoticed and undocumented until you look at a function for the 11th\n\ttime: Its return value is undefined if BGM is inactive – that is, if the\n\tuser disabled it, or if no FM board is installed. Not that it matters for\n\tthe original code, which never uses this function to retrieve anything from\n\tKAJA's drivers. But people apparently do copy ReC98 code into their own\n\tprojects, so it \u003ci\u003eis\u003c/i\u003e something to keep in mind.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAll in all, nothing quite at \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/jank\" title=\"Code of questionable quality.\"\u003ejank\u003c/a\u003e\u003c/span\u003e level in this one, but we were surely \u003ci\u003egrazing\u003c/i\u003e that tag. Next up, with that out of the way: The bullet update/step function! Very soon in fact, since I've mostly got it done already.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-07-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-07-20T22:35:20Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-06-21",
      "url": "https://rec98.nmlgc.net/blog/2021-06-21",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-07-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-06-21\"\u003e\u003ctime datetime=\"2021-06-21T13:49:31Z\"\u003e2021-06-21 13:49\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0147\"\u003eP0147\u003c/a\u003e\n\t\t\tTH04/TH05 RE (.BB 16×16 tile animations) / TH05 decompilation (Shinki + EX-Alice backgrounds)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/456b621...c940059\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/ex-alice\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Extra Stage boss.\"\u003eex-alice\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tDidn't \u003ci\u003equite\u003c/i\u003e get to cover background rendering for TH05's Stage 1-5\n\tbosses in this one, as I had to reverse-engineer two more fundamental parts\n\tinvolved in boss background rendering before.\n\u003c/p\u003e\u003cp\u003e\n\tFirst, we got the those blocky transitions from stage tiles to bomb and\n\tboss backgrounds, loaded from \u003ccode\u003eBB*.BB\u003c/code\u003e and \u003ccode\u003eST*.BB\u003c/code\u003e,\n\trespectively. These files store 16 frames of animation, with every bit\n\tcorresponding to a 16×16 tile on the playfield. With 384×368 pixels to be\n\tcovered, that would require 69 bytes per frame. But since that's a very odd\n\tnumber to work with in micro-optimized ASM, ZUN instead stores 512×512\n\tpixels worth of bits, ending up with a frame size of 128 bytes, and a\n\tper-frame waste of 59 bytes. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e At least it was\n\tpossible to decompile the core blitting function as \u003ccode\u003e__fastcall\u003c/code\u003e\n\tfor once. \u003cbr\u003e\n\tBut wait, TH05 comes with, and loads, a bomb .BB file for every character,\n\tnot just for the Reimu and Yuuka bomb transitions you see in-game… 🤔\n\tRestoring those unused stage tile\u0026nbsp;→\u0026nbsp;bomb image transition\n\tanimations for Mima and Marisa isn't that trivial without having decompiled\n\ttheir actual bomb animation functions before, so stay tuned!\n\u003c/p\u003e\u003cp\u003e\n\tInterestingly though, the code leaves out what would look like the most\n\tobvious optimization: \u003ci\u003eAll\u003c/i\u003e stage tiles are unconditionally redrawn\n\teach frame before they're erased again with the 16×16 blocks, no matter if\n\tthey weren't covered by such a block in the previous frame, or \u003ci\u003eare\u003c/i\u003e\n\tgoing to be covered by such a block in \u003ci\u003ethis\u003c/i\u003e frame. The same is true\n\tfor the static bomb and boss background images, where ZUN simply didn't\n\twrite a .CDG blitting function that takes the dirty tile array into\n\taccount. If VRAM writes on PC-98 really were as slow as the games'\n\t\u003ccode\u003eREADME.TXT\u003c/code\u003e files claim them to be, shouldn't \u003ci\u003eall\u003c/i\u003e the\n\toptimization work have gone towards minimizing them? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\tOh well, it's not like I have any idea what I'm talking about here. I'd\n\tbetter stop talking about anything relating to VRAM performance on PC-98…\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSecond, it finally was time to solve the long-standing confusion about all\n\tthose callbacks that are supposed to render the playfield background. Given\n\tthe aforementioned static bomb background images, ZUN chose to make this\n\tneedlessly complicated. And so, we have \u003ci\u003etwo\u003c/i\u003e callback function\n\tpointers: One \u003ci\u003eduring\u003c/i\u003e bomb animations, one \u003ci\u003eoutside\u003c/i\u003e of bomb\n\tanimations, and each boss update function is responsible for keeping the\n\tformer in sync with the latter. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tOther than that, this was one of the smoothest pushes we've had in a while;\n\tthe hardest parts of boss background rendering all were part of\n\t\u003ca href=\"/blog/2021-06-10\"\u003e📝 the last push\u003c/a\u003e. Once you figured out that\n\tZUN does indeed dynamically change hardware color #0 based on the current\n\tboss phase, the remaining one function for Shinki, and all of EX-Alice's\n\tbackground rendering becomes very straightforward and understandable.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tMeanwhile, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e told me about his plans to publicly\n\trelease \u003ca href=\"/blog/2020-09-17\"\u003e📝 his TH05 scripting toolkit\u003c/a\u003e once\n\tTH05's \u003ccode\u003eMAIN.EXE\u003c/code\u003e would hit around 50% RE! That pretty much\n\tdefines what the next bunch of generic TH05 pushes will go towards:\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/bullet\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/a\u003e\u003c/span\u003es, shared \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/boss\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/a\u003e\u003c/span\u003e code, and one\n\tfull, concrete boss script to demonstrate how it's all combined. Next up,\n\ttherefore: TH04's bullet firing code…? Yes, TH04's. I want to see what I'm\n\tdoing before I tackle the undecompilable mess that is TH05's bullet firing\n\tcode, and \u003ci\u003eyou\u003c/i\u003e all probably want readable code for that feature as\n\twell. Turns out it's also the perfect place for Blue Bolt's\n\tpending contributions.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-07-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-10\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-06-21T13:49:31Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-06-10",
      "url": "https://rec98.nmlgc.net/blog/2021-06-10",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-06-10\"\u003e\u003ctime datetime=\"2021-06-10T20:56:05Z\"\u003e2021-06-10 20:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0146\"\u003eP0146\u003c/a\u003e\n\t\t\tTH05 decompilation (Shinki's background animations) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/08bc188...456b621\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tY'know, I kinda prefer the pending crowdfunded workload to stay more near\n\tthe middle of the cap, rather than being sold out all the time. So to reach\n\tthis point more quickly, let's do the most relaxing thing that can be\n\teasily done in TH05 right now: The boss backgrounds, starting with Shinki's,\n\t\u003ca href=\"/blog/2020-08-19\"\u003e📝 now that we've got the time to look at it in detail\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\t… Oh \u003ci\u003ecome on\u003c/i\u003e, more things that are borderline undecompilable, and\n\trequire new workarounds to be developed? Yup, Borland C++ always optimizes\n\tany comparison of a register with a literal 0 to \u003ccode\u003eOR reg, reg\u003c/code\u003e,\n\tno matter how many calculations and inlined function calls you replace the\n\t0 with. Shinki's background particle rendering function contains a\n\t\u003ccode\u003eCMP AX, 0\u003c/code\u003e instruction though… so yeah,\n\t\u003ca href=\"/blog/2021-02-21\"\u003e📝 yet another piece of custom ASM that's worse\u003c/a\u003e\n\tthan what Turbo C++ 4.0J would have generated if ZUN had just written\n\treadable C. This was probably motivated by ZUN insisting that his modified\n\tmaster.lib function for blitting particles takes its X and Y parameters as\n\tregisters. If he had just used the \u003ccode\u003e__fastcall\u003c/code\u003e convention, he\n\talso would have got the sprite ID passed as a register. 🤷\u003cbr\u003e\n\tSo, we \u003ci\u003ereally\u003c/i\u003e don't want to be forced into inline assembly just\n\tbecause of the third comparison in the otherwise perfectly decompilable\n\tfour-comparison \u003ccode\u003eif()\u003c/code\u003e expression that prevents invisible\n\tparticles from being drawn. The workaround: Comparing to a \u003ci\u003epointer\u003c/i\u003e\n\tinstead, which only the linker gets to resolve to the actual value of 0.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e This way, the compiler has to make room for\n\tany 16-bit literal, and can't optimize anything.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd then we go straight from \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/micro-optimization\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/a\u003e\u003c/span\u003e to\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/waste\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/a\u003e\u003c/span\u003e, with all the duplication in the code that\n\tanimates all those particles together with the zooming and spinning lines.\n\tThis push decompiled 1.31% of all code in TH05, and thanks to alignment,\n\twe're still missing Shinki's high-level background rendering function that\n\tcalls all the subfunctions I decompiled here.\u003cbr\u003e\n\tWith all the manipulated state involved here, it's not at all trivial to\n\tsee how this code produces what you see in-game. Like:\u003col\u003e\n\t\u003cli\u003eIf all lines have the same Y velocity, how do the other three lines in\n\tbackground type B get pushed down into this vertical formation while the\n\ttop one stays still? (Answer: This velocity is only applied to the top\n\tline, the other lines are only pushed based on some delta.)\u003c/li\u003e\n\t\u003cli\u003eHow can this delta be calculated based on the distance of the top line\n\twith its supposed target point around Shinki's wings? (Answer: The velocity\n\tis never set to 0, so the top line overshoots this target point in every\n\tframe. After calculating the delta, the top line itself is pushed down as\n\twell, canceling out the movement. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e)\u003c/li\u003e\n\t\u003cli\u003eWhy don't they get pushed down infinitely, but stop eventually?\n\t(Answer: We only see four lines out of 20, at indices #0, #6, #12, and\n\t#18. In each frame, lines [0..17] are copied to lines [1..18], before\n\tanything gets moved. The invisible lines are pushed down based on the delta\n\tas well, which defines a distance between the visible lines of (velocity *\n\tarray gap). And since the velocity is capped at -14 pixels per frame, this\n\talso means a maximum distance of 84 pixels between the midpoints of each\n\tline.)\u003c/li\u003e\n\t\u003cli\u003eAnd why are the lines moving back up when switching to background type\n\tC, before moving down? (Answer: Because type C \u003ci\u003eincreases\u003c/i\u003e the\n\tvelocity rather than decreasing it. Therefore, it relies on the previous\n\tvelocity state from type B to show a gapless animation.)\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tSo yeah, it's a nice-looking effect, just \u003ci\u003every\u003c/i\u003e hard to understand. 😵\n\u003c/p\u003e\u003cp\u003e\n\tWith the amount of effort I'm putting into this project, I typically\n\tgravitate towards more descriptive function names. Here, however,\n\tuth05win's simple and seemingly tiny-brained \"background type A/B/C/D\" was\n\tquite a smart choice. It clearly defines the sequence in which these\n\tanimations are intended to be shown, and as we've seen with point 4\n\tfrom the list above, that does indeed matter.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: At least EX-Alice's background animations, and probably also the\n\thigh-level parts of the background rendering for all the other TH05 bosses.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-06-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-06-10T20:56:05Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-06-06",
      "url": "https://rec98.nmlgc.net/blog/2021-06-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-06-06\"\u003e\u003ctime datetime=\"2021-06-06T00:53:22Z\"\u003e2021-06-06 00:53\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0143\"\u003eP0143\u003c/a\u003e\n\t\t\tWebsite (Progress number caching) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/9069fb7...c8ac7e5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0144\"\u003eP0144\u003c/a\u003e\n\t\t\tWebsite (Blog tag system, part 1: Manual and automatic tag assignment, blog filtering, design) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/c8ac7e5...69dd597\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0145\"\u003eP0145\u003c/a\u003e\n\t\t\tWebsite (Blog tag system, part 2: Combining tags, per-tag descriptions) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/69dd597...71417b6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Yanga, \u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWho said working on the website was \"fun\"? That code is a mess.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e This right here is the first time I seriously\n\twrote a website from (almost) scratch. Its main job is to parse over a Git\n\trepository and calculate numbers, so any additional bulky frameworks would\n\tonly be in the way, and probably need to be run on some sort of wobbly,\n\tunmaintainable \"stack\" anyway, right? 😛\n\t\u003ca href=\"/blog/2020-09-17\"\u003e📝 As with the main project\u003c/a\u003e though, I'm only\n\tbeginning to figure out the best structure for this, and these new features\n\tprompted quite a lot of upfront refactoring…\n\u003c/p\u003e\u003cp\u003e\n\tBefore I start ranting though, let's quickly summarize the most visible\n\tchange, the new tag system for this blog!\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eYes, I manually went through every one of the 82 posts I've written so\n\tfar, and assigned labels to them.\u003c/li\u003e\n\t\u003cli\u003eThe per-project (\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/rec98\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/a\u003e\u003c/span\u003e and\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/website\" title=\"Code behind this website.\"\u003ewebsite\u003c/a\u003e\u003c/span\u003e) and per-game (\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/th01\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/a\u003e\u003c/span\u003e\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/th02\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/a\u003e\u003c/span\u003e\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/th03\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/a\u003e\u003c/span\u003e\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/th04\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/a\u003e\u003c/span\u003e\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/th05\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/a\u003e\u003c/span\u003e) tags are automatically generated from the\n\tdatabase and the Git commit history, respectively. That might have\n\tended us up with a fair bit of category clutter, as any single change\n\tto a tiny aspect is enough for a blog post to be tagged with an\n\totherwise unrelated game. For now, it doesn't seem \u003ci\u003etoo\u003c/i\u003e much of\n\tan issue though.\u003c/li\u003e\n\t\u003cli\u003eFiltering already works for an arbitrary number of tags. Right now,\n\tthese are always combined with \u003ccode class=\"hovertext\"\n\t\ttitle=\"Or, if formal logic is your thing, a conjunction. ∧\"\n\t\u003eAND\u003c/code\u003e – no arbitrary boolean expressions for tag filtering yet.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003c/li\u003e\n\t\u003cli\u003eAdding filters simply works by adding components to the URL path:\n\t\u003ckbd\u003ehttps://rec98.nmlgc.net/blog/tag/tag1/tag2/tag3/\u003c/kbd\u003e… and so\n\ton.\u003c/li\u003e\n\t\u003cli\u003eHovering over any tag shows a brief description of what that tag is\n\tabout. Some of the terms really needed a definition, so I just added one for\n\tall of them. Hope you all enjoy them!\u003c/li\u003e\n\t\u003cli\u003eThese descriptions are also shown on the new\n\t\u003ca href=\"/blog/tag\"\u003etag overview page\u003c/a\u003e, which now kind of doubles as a\n\tglossary.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tFinally, the order page now shows the exact number of pushes a contribution\n\twill fund – no more manual \u003ca href=\"/faq#duration\"\u003edivisions\u003c/a\u003e required.\n\tShoutout to the one email I received, which pointed out this potential\n\timprovement!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs for the \"invisible\" changes: The one main feature of this website, the\n\taforementioned calculation of the progress metrics, also turned out as its\n\tbiggest annoyance over the years. It takes a little while to parse all the\n\tbig .ASM files in the source tree, once for every push that can affect the\n\taverage number of removed instructions and unlabeled addresses. And without\n\ta cache, we've had to do \u003ci\u003ethat\u003c/i\u003e every time we re-launch the app server\n\tprocess.\u003cbr\u003e\n\tFundamentally, this is – you might have guessed it – a dependency tracking\n\tproblem, with two inputs: the .ASM files from the ReC98 repo, and the\n\tGolang code that calculates the instruction and PI numbers. Sure, the code\n\thas been pretty stable, but what if we do end up extending it one day? I've\n\talways disliked manually specified version numbers for use cases like this\n\tone, where the problem at hand could be exactly solved with a hashing\n\tfunction, without being prone to human error.\n\u003c/p\u003e\u003cp\u003e\n\t(Sidenote: That's why I never actively supported thcrap mods that affected\n\tgameplay while I was still working on that project. We still want to be\n\table to save and share replays made on modded games, but I do \u003ci\u003enot\u003c/i\u003e\n\twant to subject users to the unacceptable burden of manually remembering\n\twhich version of which patch stack they've recorded a given replay with.\n\tSo, we'd somehow need to calculate a hash of everything that defines the\n\t\u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/gameplay\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/a\u003e\u003c/span\u003e, exclude the things that don't, and only show\n\treplays that were recorded on the hash that matches the currently running\n\tpatch stack. Well, turns out that True Touhou Fans™ quite enjoy watching\n\tthe games get broken in every possible way. That's the way ZUN intended the\n\tgames to be experienced, after all. Otherwise, he'd be constantly\n\tmaintaining the games and shipping bugfix patches… 🤷)\n\u003c/p\u003e\u003cp\u003e\n\tNow, why haven't I been caching the progress numbers all along? Well,\n\tparallelizing that parsing process onto all available CPU cores seemed\n\tenough in 2019 when this site launched. Back then, the estimates were\n\tcalculated from slightly over 10 million lines of ASM, which took about 7\n\tseconds to be parsed on my mid-range dev system.\u003cbr\u003e\n\tFast forward to P0142 though, and we have to parse 34.3 million lines of\n\tASM, which takes about 26 seconds on my dev system. That would have only\n\tgot worse with every new delivery, especially since this production server\n\tdoesn't have as many cores.\n\u003c/p\u003e\u003cp\u003e\n\tI was thinking about a \"doing less\" approach for a while: Parsing only the\n\tfiles that had changed between the start and end commit of a push, and\n\tkeeping those deltas across push boundaries. However, that turned out to be\n\t\u003ci\u003eslightly\u003c/i\u003e more complex than the few hours I wanted to spend on it.\n\tAnd who knows how well \u003ci\u003ethat\u003c/i\u003e would have scaled. We've still got a few\n\thundred pushes left to go before we're done here, after all.\n\u003c/p\u003e\u003cp\u003e\n\tSo with the tag system, as always, taking longer and consuming more pushes\n\tthan I had planned, the time had come to finally address the underlying\n\tdependency tracking problem.\u003cbr\u003e\n\tInitially, this sounded like a nail that was tailor-made for\n\t\u003ca href=\"/blog/2020-09-03\"\u003e📝 my favorite hammer, Tup\u003c/a\u003e: Move the parser\n\tto a separate binary, gather the list of all commits via \u003ccode\u003egit\n\trev-list\u003c/code\u003e, and run that parser binary on every one of the commits\n\treturned. That should end up correctly tracking the relevant parts of\n\t\u003ccode\u003e.git/\u003c/code\u003e and the new binary as inputs, and cause the commits to\n\tbe re-parsed if the parser binary changes, right? Too bad that Tup both\n\t\u003ca href=\"https://github.com/gittup/tup/issues/238\"\u003erefuses to track\n\tanything inside \u003ccode\u003e.git/\u003c/code\u003e\u003c/a\u003e, and can't track a Golang binary\n\teither, due to all of the compiler's unpredictable outputs into its build\n\tcache. But can't we at least turn off–\n\u003c/p\u003e\u003cblockquote\u003e\u003e The build cache is now required as a step toward eliminating \u003ccode\u003e$GOPATH/pkg\u003c/code\u003e.\n\t— \u003ca href=\"https://golang.org/doc/go1.12#gocache\"\u003eGo 1.12 release notes\u003c/a\u003e\n\u003c/blockquote\u003e\u003cp\u003e\n\tOh, \u003ci\u003ewonderful\u003c/i\u003e. Hey, I always liked \u003ccode\u003e$GOPATH\u003c/code\u003e! 🙁\n\u003c/p\u003e\u003cp\u003e\n\tBut sure, Golang is too smart anyway to require an external build system.\n\tThe compiler's\n\t\u003ca href=\"https://golang.org/src/cmd/go/internal/work/buildid.go\"\u003ebuild\n\tID\u003c/a\u003e is exactly what we need to correctly invalidate the progress number\n\tcache. Surely there is a way to retrieve the build ID for any package that\n\tmakes up a binary at runtime via some kind of reflection, right? Right? …Of\n\t\u003ci\u003ecourse\u003c/i\u003e not, in the great Unix tradition, this functionality is only\n\tavailable as a CLI tool that prints its result to \u003ccode\u003estdout\u003c/code\u003e.\n\t🙄\u003cbr\u003e\n\tBut sure, no problem, let's just \u003ccode\u003eexec()\u003c/code\u003e a separate process on\n\tthe parser's library package file… oh wait, such a thing doesn't exist\n\tanymore, unless you manually \u003ckbd\u003einstall\u003c/kbd\u003e the package. This would\n\thave added another complication to the build process, \u003ci\u003eand\u003c/i\u003e you'd\n\tstill have to manually locate the package file, with its version-specific\n\tdirectory name. That \u003ci\u003emight\u003c/i\u003e have worked out in the end, but figuring\n\tall this out would have probably gone way beyond the budget.\n\u003c/p\u003e\u003cp\u003e\n\tOK, but who cares about packages? We just care about one single file here,\n\tanyway. Didn't they put the official Golang source code parser into the\n\tstandard library? Maybe \u003ci\u003ethat\u003c/i\u003e can give us something close to the\n\tbuild ID, by hashing the abstract syntax tree of that file. Well, for\n\tstarters, one does not simply \u003ci\u003eserialize\u003c/i\u003e the returned AST. At least\n\tinto Golang's own, most \"native\" \u003ca hreF=\"https://blog.golang.org/gob\"\u003eGob\n\tformat\u003c/a\u003e, which requires all types from the \u003ccode\u003ego/ast\u003c/code\u003e package\n\tto be manually registered first.\u003cbr\u003e\n\tThat leaves\n\t\u003ca href=\"https://golang.org/pkg/go/ast/#Fprint\"\u003east.Fprint()\u003c/a\u003e as the\n\tonly thing close to a ready-made serialization function… and guess what,\n\tthat one suffers from Golang's typical non-deterministic order when\n\trendering any map to a string. 🤦\n\u003c/p\u003e\u003cp\u003e\n\tGuess there's no way around the simplest, most stupid way of simply\n\tcalculating any cryptographically secure hash over the ASM parser file. 😶\n\tIt's not like we frequently change comments in this file, but still, this\n\tcould have been so much nicer.\u003cbr\u003e\n\tOh well, at least I \u003ci\u003edid\u003c/i\u003e get that issue resolved now, in an\n\tacceptable way. If you ever happened to see this website rebuilding: That\n\tshould now be a matter of seconds, rather than minutes. Next up: Shinki's\n\tbackground animations!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-10\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-06-06T00:53:22Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-05-27",
      "url": "https://rec98.nmlgc.net/blog/2021-05-27",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-05-27\"\u003e\u003ctime datetime=\"2021-05-27T19:25:15Z\"\u003e2021-05-27 19:25\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0140\"\u003eP0140\u003c/a\u003e\n\t\t\tResearch (PC-98 DOS graph mode, with implementation into DOSBox-X) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d985811...d856f7d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0141\"\u003eP0141\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 1/5.5: Entrance animation)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d856f7d...5afee78\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0142\"\u003eP0142\u003c/a\u003e\n\t\t\tTH01 decompilation (Konngara, part 2/5.5: Rendering, pattern 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5afee78...08bc188\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], rosenrose, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/emulation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Features and bugs in various PC-98 and DOS emulators.\"\u003eemulation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/dosbox-x\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A DOSBox fork with support for PC-98 emulation.\"\u003edosbox-x\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/danmaku-pattern\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A complete danmaku animation over a specified period of time.\"\u003edanmaku-pattern\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAlright, onto Konngara! Let's quickly move the escape sequences used later\n\tin the battle to C land, and then we can immediately decompile the loading\n\tand entrance animation function together with its filenames. Might as well\n\treverse-engineer those escape sequences while I'm at it, though – even if\n\tthey aren't implemented in DOSBox-X, they're well documented in all those\n\tJapanese PDFs, so this should be no big deal…\n\u003c/p\u003e\u003cp\u003e\n\t…wait, \u003ckbd\u003eESC )3\u003c/kbd\u003e switches to \u003ci\u003e\"graph mode\"\u003c/i\u003e? As opposed to the\n\tdefault \u003ci\u003e\"kanji mode\"\u003c/i\u003e, which can be re-entered via \u003ckbd\u003eESC )0\u003c/kbd\u003e?\n\tLet's look up graph mode in the \u003ci\u003ePC-9801 Programmers' Bible\u003c/i\u003e then…\n\u003c/p\u003e\u003cblockquote\u003e\u003e Kanji cannot be handled in this mode.\n\u003c/blockquote\u003e\u003cp\u003e\n\t…and that's apparently all it has to say. Why have it then, on a platform\n\twhose main selling point is a kanji ROM, and where Shift-JIS (and, well,\n\t7-bit ASCII) are the only native encodings? No support for graph mode in\n\tDOSBox-X either… yeah, let's take a deep dive into NEC's\n\t\u003ccode\u003eIO.SYS\u003c/code\u003e, and get to the bottom of this.\n\u003c/p\u003e\u003cp\u003e\n\tAnd yes, graph mode pretty much just disables Shift-JIS decoding for\n\tcharacters written via \u003ccode\u003eINT 29h\u003c/code\u003e, the lowest-level way of \"just\n\tprinting a \u003ccode\u003echar\u003c/code\u003e\" on DOS, which every \u003ccode\u003eprintf()\u003c/code\u003e\n\twill ultimately end up calling. Turns out there is a use for it though,\n\twhich we can spot by looking at the 8×16 half-width section of font ROM:\u003c/p\u003e\n\t\u003cfigure\u003e\u003ca href=\"/blog/static/2021-05-27-PC98-8x16-font-ROM.png?5ca3b5a2\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2021-05-27-PC98-8x16-font-ROM.png?5ca3b5a2\"\n\t\talt=\"8×16 half-width section of font ROM, with the characters in the Shift-JIS lead byte range highlighted in red\"\n\t\u003e\u003c/a\u003e\u003c/figure\u003e\u003cp\u003e\n\tThe half-width glyphs marked in \u003cspan style=\"color: red;\"\u003ered\u003c/span\u003e\n\tcorrespond to the byte ranges from 0x80-0x9F and 0xE0-0xFF… which Shift-JIS\n\tdefines as lead bytes for two-byte, full-width characters. But if we turn\n\t\u003ci\u003eoff\u003c/i\u003e Shift-JIS decoding…\u003c/p\u003e\n\t\u003cfigure\u003e\u003ca href=\"/blog/static/2021-05-27-PC98-text-modes.png?3ef75c80\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2021-05-27-PC98-text-modes.png?3ef75c80\"\n\t\talt=\"Visible differences between the kanji and graph modes on PC-98 DOS\"\n\t\u003e\u003c/a\u003e\u003cfigcaption\u003e\n\t\t(Yes, that \u003ccode\u003eg\u003c/code\u003e in the function row is how NEC DOS\n\t\tindicates that graph mode is active. Try it yourself by pressing\n\t\t\u003ckbd\u003eCtrl+F4\u003c/kbd\u003e!)\n\t\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003e\n\tJackpot, we get those half-width characters when printing their\n\tcorresponding bytes.\u003cbr\u003e\n\t\u003ca href=\"https://github.com/joncampbell123/dosbox-x/pull/2547\"\u003eI've\n\tre-implemented all my findings into DOSBox-X\u003c/a\u003e, which will include graph\n\tmode in the upcoming 0.83.14 release. If P0140 looks a bit empty as a\n\tresult, that's why – most of the immediate feature work went into\n\tDOSBox-X, not into ReC98. That's the beauty of \"anything\" pushes.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tSo, after switching to graph mode, TH01 does… one of the slowest possible\n\t\u003ccode\u003ememset()\u003c/code\u003es over all of text RAM – one \u003ccode\u003eprintf(\" \")\u003c/code\u003e\n\tcall for every single one of its 80×25 half-width cells – before switching\n\tback to kanji mode. What a waste of RE time…? Oh well, at least we've now\n\tgot plenty of proof that these weird escape sequences \u003ci\u003eactually\u003c/i\u003e do\n\tnothing of interest.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAs for the Konngara code itself… well, it's script-like code, what can you\n\tsay. Maybe minimally sloppy in some places, but ultimately harmless.\u003cbr\u003e\n\tOne small thing that might not be widely known though: The large,\n\tblue-green Siddhaṃ seed syllables are supposed to show up immediately, with\n\tno delay between them? Good to know. Clocking your emulator too low tends\n\tto roll them down from the top of the screen, and will certainly add a\n\tnoticeable delay between the four individual images.\n\u003c/p\u003e\u003cp\u003e\n\t… Wait, but this means that ZUN could have \u003ci\u003eintended\u003c/i\u003e this \"effect\".\n\tWhy else would he not only put those syllables into four individual images\n\t(and therefore add at least the latency of disk I/O between them), but also\n\tshow them on the foreground VRAM page, rather than on the \"back buffer\"?\n\u003c/p\u003e\u003cp\u003e\n\tMeanwhile, in \u003ca href=\"/blog/2020-11-16\"\u003e📝 another\u003c/a\u003e instance of \"maybe\n\thaving gone too far in a few places\":\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/08bc188e7dae7da9598f389d0bd8fa9da84db02d/th01/main/boss/b20j.cpp#L496\"\u003eExpressing distances on the playfield as fractions of its width\n\tand height, just to avoid absolute numbers\u003c/a\u003e? Raw numbers are bad because\n\tthey're in screen space in this game. But we've already been throwing\n\t\u003ccode\u003ePLAYFIELD_\u003c/code\u003e constants into the mix as a way of explicitly\n\tcommunicating screen space, and keeping raw number literals for the actual\n\tplayfield coordinates is looking increasingly sloppy… I don't know,\n\tfractions really seemed like the most sensible thing to do with what we're\n\tgiven here. 😐\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, 2 pushes in, and we've got the loading code, the entrance animation,\n\tfacial expression rendering, and the first one out of Konngara's 12\n\tdanmaku patterns. Might not sound like much, but since that first pattern\n\tinvolves those\n\t\u003cimg src=\"data:image/gif;base64,R0lGODlhCAAIAIABAACZqv///yH5BAEKAAEALAAAAAAIAAgAAAINjAGmiMv9okzSNRVuKAA7\" alt=\"◆\"\u003e\n\tblue-green diamond sprites and therefore is one of the more complicated\n\tones, it all amounts to roughly 21.6% of Konngara's code. That's 7 more\n\tpushes to get Konngara done, then? Next up though: Two pushes of website\n\timprovements.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-06-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-05-27T19:25:15Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-05-13",
      "url": "https://rec98.nmlgc.net/blog/2021-05-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-05-13\"\u003e\u003ctime datetime=\"2021-05-13T14:00:00Z\"\u003e2021-05-13\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t\u003cstrong\u003eLast updated: \u003ca href=\"/blog/2025-12-31\"\u003e📝 2025-12-31\u003c/a\u003e\u003c/strong\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSecured a 20-hour RL workweek to leave plenty of time for this project,\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e's commissioned MediaWiki update work is also nearing\n\tcompletion, time to reopen the store! Since it's been a long time, here's\n\tan overview of where we currently are in each game and binary, and what\n\tthe next logical step would be:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003cstrong\u003eTH02:\u003c/strong\u003e\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAIN.EXE:\u003c/code\u003e The final PC-98-specific low-level rendering functions are blocked by a single inconsistent and thus undecompilable assembly instruction. Rather than going for \u003ca href=\"/blog/2023-01-17\"\u003e📝 code generation\u003c/a\u003e and turning the rest of the function into a mess, I'd like to introduce a new build step between compilation and linking to patch \"mistakes\" like these. This would be an investment of 1-2 pushes into more readable code. Otherwise, I could continue with\u003cul\u003e\n\t\t\t\u003cli\u003eplayer shots and the control code for the three shot types,\u003c/li\u003e\n\t\t\t\u003cli\u003elasers,\u003c/li\u003e\n\t\t\t\u003cli\u003eplayer character movement, or\u003c/li\u003e\n\t\t\t\u003cli\u003ebomb rendering.\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAINE.EXE:\u003c/code\u003e \u003ccode\u003emain()\u003c/code\u003e and the congratulation picture screens.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003eTH03:\u003c/strong\u003e\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAIN.EXE:\u003c/code\u003e The enemy/fireball/explosion structure is already scheduled to come out in early January 2026. We're still missing a lot of essential gameplay structures beyond that, but \u003ca href=\"/blog/2025-12-31#playchars-2025-12-31\"\u003e📝 decompilation is expected to progress \u003ci\u003every\u003c/i\u003e quickly once we reach character-specific code\u003c/a\u003e.\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAINL.EXE:\u003c/code\u003e The stage start screen, which will be nice to have for the \u003ca href=\"/blog/2023-07-28\"\u003e📝 upcoming multilingual translations commissioned by Touhou Patch Center\u003c/a\u003e.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003eTH04:\u003c/strong\u003e\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003eOP.EXE:\u003c/code\u003e Finalizing the ZUN Soft logo animation, and then we're done!\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAIN.EXE:\u003c/code\u003e\n\t\t\tThere's a lot of partially reverse-engineered but not yet decompiled code, including all of this game's custom entity types. Covering that would significantly boost finalization% for very little money. Otherwise, we could go for either\u003cul\u003e\n\t\t\t\t\u003cli\u003eGengetsu's boss script\u003c/li\u003e\n\t\t\t\t\u003cli\u003eHUD rendering (in parallel with TH05)\u003c/li\u003e\n\t\t\t\t\u003cli\u003eplayer update code (in parallel with TH05), required for all midbosses in this game\u003c/li\u003e\n\t\t\t\t\u003cli\u003eplayer shot control functions\u003c/li\u003e\n\t\t\t\t\u003cli\u003ethe end-of-stage bonus calculation\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAINE.EXE:\u003c/code\u003e High score name entry.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003e\u003cstrong\u003eTH05:\u003c/strong\u003e\u003cul\u003e\n\t\t\u003cli\u003e\u003ccode\u003eOP.EXE:\u003c/code\u003e Finalizing the ZUN Soft logo animation, and then we're done!\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAIN.EXE:\u003c/code\u003e Got quite a lot of segment splits where we could immediately continue:\u003cul\u003e\n\t\t\t\u003cli\u003emidboss and boss script code, either continuing with the in-game order and the Stage 2 midboss fight, or going directly for\u003cul\u003e\n\t\t\t\t\u003cli\u003eMai \u0026 Yuki,\u003c/li\u003e\n\t\t\t\t\u003cli\u003ethe Extra Stage midboss,\u003c/li\u003e\n\t\t\t\t\u003cli\u003eor EX-Alice\u003c/li\u003e\n\t\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\t\t\u003cli\u003estage tile rendering\u003c/li\u003e\n\t\t\t\u003cli\u003eplayer update code (in parallel with TH04)\u003c/li\u003e\n\t\t\t\u003cli\u003eitems and extends\u003c/li\u003e\n\t\t\t\u003cli\u003ebomb rendering\u003c/li\u003e\n\t\t\t\u003cli\u003ethe single-color areas of boss backgrounds, which use the GRCG's TDW mode\u003c/li\u003e\n\t\t\t\u003cli\u003eHUD rendering (in parallel with TH05)\u003c/li\u003e\n\t\t\t\u003cli\u003eboss sprite rendering boilerplate\u003c/li\u003e\n\t\t\t\u003cli\u003eplayer shot collision detection and rendering\u003c/li\u003e\n\t\t\u003c/ul\u003e\u003c/li\u003e\n\t\t\u003cli\u003e\u003ccode\u003eMAINE.EXE:\u003c/code\u003e The staff roll animation.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tBut as always, you can request pretty much any other part of any game.\n\tWe're now at a pretty good place as far as arbitrary requests are\n\tconcerned, as I simply can't decide myself where to put all the current\n\tpending contributions in the \u003ca href=\"/fundlog\"\u003efunding backlog\u003c/a\u003e. 😅 By\n\tspending only the missing amount of money to complete any of those, you can\n\tcapture any of those \"fractional\" contributions towards a specific goal.\n\u003c/p\u003e\u003cp\u003e\n\tThe next specific requests are going to set the priorities of this project\n\tfor quite some time! The best strategy: Spend a low amount of money on\n\tsomething very specific, and watch as existing generic contributions will\n\tnecessarily have to be put towards making that specific goal happen 😛\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-05-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-05-13T16:00:00+02:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-05-12",
      "url": "https://rec98.nmlgc.net/blog/2021-05-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-04-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-05-12\"\u003e\u003ctime datetime=\"2021-05-12T18:01:20Z\"\u003e2021-05-12 18:01\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0139\"\u003eP0139\u003c/a\u003e\n\t\t\tSeparating translation units, part 10/10 (TH04/TH05 sound initialization / TH04 PMD loading)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/864e864...d985811\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTechnical debt, part 10… in which two of the PMD-related functions came\n\twith such complex ramifications that they required one full push after\n\tall, leaving no room for the additional decompilations I wanted to do. At\n\tleast, this \u003ci\u003edid\u003c/i\u003e end up being the final one, completing all\n\t\u003ccode\u003eSHARED\u003c/code\u003e segments for the time being.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe first one of these functions determines the BGM and sound effect\n\tmodes, combining the resident type of the PMD driver with the Option menu\n\tsetting. The TH04 and TH05 version is apparently coded quite smartly, as\n\tPC-98 Touhou only needs to distinguish \u003ci\u003e\"OPN-\u0026nbsp;/\n\tPC-9801-26K-compatible sound sources handled by \u003ccode\u003ePMD.COM\u003c/code\u003e\"\u003c/i\u003e\n\tfrom \u003ci\u003e\"everything else\"\u003c/i\u003e, since all other PMD varieties are\n\tOPNA-\u0026nbsp;/ PC-9801-86-compatible.\u003cbr\u003e\n\tTherefore, I only documented those two results returned from PMD's\n\t\u003ccode\u003eAH=09h\u003c/code\u003e function. I'll leave a comprehensive, fully documented\n\tenum to interested contributors, since that would involve research into\n\tbasically the entire history of the PC-9800 series, and even the clearly\n\tout-of-scope PC-88VA. After all, distinguishing between more versions of\n\tthe PMD driver in the Option menu (and adding new sprites for them!) is\n\tstrictly mod territory.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe honor of being the final decompiled function in any \u003ccode\u003eSHARED\u003c/code\u003e\n\tsegment went to TH04's \u003ccode\u003esnd_load()\u003c/code\u003e. TH04 contains by far the\n\tsanest version of this function: Readable C code, no new ZUN bugs (and\n\tstill missing file I/O error handling, of course)… but wait, what about\n\tthat actual file read syscall, using the \u003ccode\u003eINT 21h, AH=3Fh\u003c/code\u003e DOS\n\tfile read API? Reading up to a hardcoded number of bytes into PMD's or\n\tMMD's song or sound effect buffer, 20\u0026nbsp;KiB in TH02-TH04, 64\u0026nbsp;KiB in\n\tTH05… that's kind of weird. About time we looked closer into this.\n\t\u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tTurns out that no, KAJA's driver doesn't give you the full 64 KiB of one\n\tmemory segment for these, as especially TH05's code might suggest to\n\tanyone  unfamiliar with these drivers. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Instead,\n\tyou can customize the size of these buffers on its command line. In\n\t\u003ccode\u003eGAME.BAT\u003c/code\u003e, ZUN allocates 8 KiB for FM songs, 2 KiB for sound\n\teffects, and 12 KiB for MMD files in TH02… which means that the hardcoded\n\tsizes in \u003ccode\u003esnd_load()\u003c/code\u003e are completely wrong, no matter how you\n\tlook at them. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Consequently, this read syscall\n\t\u003ci\u003ewill\u003c/i\u003e overflow PMD's or MMD's song or sound effect buffer if the\n\tgiven file is larger than the respective buffer size.\u003cbr\u003e\n\tNow, ZUN could have simply hardcoded the sizes from \u003ccode\u003eGAME.BAT\u003c/code\u003e\n\tinstead, and it would have been fine. As it \u003ci\u003ealso\u003c/i\u003e turns out though,\n\tPMD has an API function (\u003ccode\u003eAH=22h\u003c/code\u003e) to retrieve the actual\n\tbuffer sizes, provided for exactly that purpose. There is little excuse\n\tnot to use it, as it also gives you PMD's default sizes if you don't\n\tspecify any yourself.\u003cbr\u003e\n\t(Unless your build process enumerates all PMD files that are part of the\n\tgame, and bakes the largest size into both \u003ccode\u003esnd_load()\u003c/code\u003e and\n\t\u003ccode\u003eGAME.BAT\u003c/code\u003e. That would even work with MMD, which doesn't have\n\tan equivalent for \u003ccode\u003eAH=22h\u003c/code\u003e.)\n\u003c/p\u003e\u003cp\u003e\n\tWhat'd be the consequence of loading a larger file then? Well, since we\n\tdon't get a full segment, let's look at the theoretical limit first.\u003cbr\u003e\n\tPMD prefers to keep both its driver code and the data buffers in a single\n\tmemory segment. As a result, the limit for the combined size of the song,\n\tinstrument, and sound effect buffer is determined by the amount of\n\t\u003ci\u003ecode\u003c/i\u003e in the driver itself. In PMD86 version 4.8o (bundled with TH04\n\tand TH05) for example, the remaining size for these buffers is exactly\n\t45,555 bytes. Being an actually good programmer who doesn't blindly trust\n\tuser input, KAJA thankfully validates the sizes given via the\n\t\u003ckbd\u003e/M\u003c/kbd\u003e, \u003ckbd\u003e/V\u003c/kbd\u003e, and \u003ckbd\u003e/E\u003c/kbd\u003e command-line options\n\tbefore letting the driver reside in memory, and shuts down with an error\n\tmessage if they exceed 40 KiB. Would have been even better if he calculated\n\tthe exact size – even in the current\n\t\u003ca href=\"http://www5.airnet.ne.jp/kajapon/tool.html\"\u003ePMD version 4.8s from\n\tJanuary 2020\u003c/a\u003e, it's still a hardcoded value (see line 8581).\u003cbr\u003e\n\tEither way: If the file is larger than this maximum, the concrete effect\n\tis down to the \u003ccode\u003eINT 21h, AH=3Fh\u003c/code\u003e implementation in the\n\tunderlying DOS version. DOS 3.3 treats the destination address as linear\n\tand reads past the end of the segment,\n\t\u003ca href=\"https://github.com/joncampbell123/dosbox-x/blob/e38fde9d2938ca7b169cdd5376d95b66091709fe/src/dos/dos.cpp#L1607\"\u003eDOS\n\t5.0 and DOSBox-X truncate the number of bytes to not exceed the remaining\n\tspace in the segment\u003c/a\u003e, and maybe there's even a DOS that wraps around\n\tand ends up overwriting the PMD driver code. In any case: You \u003ci\u003ewill\u003c/i\u003e\n\toverwrite what's after the driver in memory – typically, the game .EXE and\n\tits master.lib functions.\n\u003c/p\u003e\u003cp\u003e\n\tIt almost feels like a happy accident that this doesn't cause issues in\n\tthe original games. The largest PMD file in any of the 4 games, the -86\n\tversion of \u003ci lang=\"ja\"\u003e幽夢　～ Inanimate Dream\u003c/i\u003e, takes up 8,099 bytes,\n\tjust under the 8,192 byte limit for BGM. For modders, I'd really recommend\n\timplementing this properly, with PMD's \u003ccode\u003eAH=22h\u003c/code\u003e function and\n\terror handling, once position independence has been reached.\n\u003c/p\u003e\u003cp\u003e\n\tWhew, didn't think I'd be doing more research into KAJA's drivers during\n\tregular ReC98 development! That's probably been the final time though, as\n\tall involved functions are now decompiled, and I'm unlikely to iterate\n\tover them again.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's it! Repaid the biggest chunk of technical debt, time for some\n\tactual progress again. Next up: Reopening the store tomorrow, and waiting\n\tfor new priorities. If we got nothing by Sunday, I'm going to put the\n\tpending [Anonymous] pushes towards some work on the website.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-04-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-05-12T18:01:20Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-04-23",
      "url": "https://rec98.nmlgc.net/blog/2021-04-23",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-04-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-04-23\"\u003e\u003ctime datetime=\"2021-04-23T00:46:42Z\"\u003e2021-04-23 00:46\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0138\"\u003eP0138\u003c/a\u003e\n\t\t\tSeparating translation units, part 9/10 (focused around TH03 / TH04) + TH04 RE (.MPN format)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8d953dc...864e864\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTechnical debt, part 9… and as it turns out, it's highly impractical to\n\trepay 100% of it at this point in development. 😕\n\u003c/p\u003e\u003cp\u003e\n\tThe reason: \u003ccode\u003egraph_putsa_fx()\u003c/code\u003e, ZUN's function for rendering\n\toptionally boldfaced text to VRAM using the font ROM glyphs, in its\n\tridiculously micro-optimized TH04 and TH05 version. This one sets the\n\t\"callback function\" for applying the boldface effect by self-modifying\n\t\u003ci\u003ethe target of two \u003ccode\u003eCALL rel16\u003c/code\u003e instructions\u003c/i\u003e… because\n\tthere \u003ci\u003ereally\u003c/i\u003e wasn't any free register left for an indirect\n\t\u003ccode\u003eCALL\u003c/code\u003e, eh? The necessary distance, from the call site to the\n\tfunction itself, has to be calculated at assembly time, by subtracting the\n\ttarget function label from the call site label.\u003cbr\u003e\n\tThis usually wouldn't be a problem… if ZUN didn't store the resulting\n\tlookup tables in the \u003ccode\u003e.DATA\u003c/code\u003e segment. With code segments, we\n\tcan easily split them at pretty much any point between functions because\n\tthere are multiple of them. But there's only a single \u003ccode\u003e.DATA\u003c/code\u003e\n\tsegment, with all ZUN and master.lib data sandwiched between Borland C++'s\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Crt0\"\u003e\u003ccode\u003ecrt0\u003c/code\u003e\u003c/a\u003e at the\n\ttop, and Borland C++'s library functions at the bottom of the segment.\n\tAdding another split point would require all data after that point to be\n\tmoved to its own translation unit, which in turn requires\n\t\u003ccode\u003eEXTERN\u003c/code\u003e references in the big .ASM file to all that moved\n\tdata… in short, it would turn the codebase into an even greater\n\tmess.\u003cbr\u003e\n\tDeclaring the labels as \u003ccode\u003eEXTERN\u003c/code\u003e wouldn't work either, since\n\tthe linker can't do fancy arithmetic and is limited to simply replacing\n\taddress placeholders with one single address. So, we're now stuck with\n\tthis function at the bottom of the \u003ccode\u003eSHARED\u003c/code\u003e segment, for the\n\tforeseeable future.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWe can still continue to separate functions off the top of that segment,\n\tthough. Pretty much the only thing noteworthy there, so far: TH04's code\n\tfor loading stage tile images from .MPN files, which we hadn't\n\treverse-engineered so far, and which nicely fit into one of\n\tBlue Bolt's pending ⅓ RE contributions. Yup, we finally moved\n\tthe RE% bars again! If only for a tiny bit.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tBoth TH02 and TH05 simply store one pointer to one dynamically allocated\n\tmemory block for all tile images, as well as the number of images, in the\n\tdata segment. TH04, on the other hand, reserves memory for 8 .MPN slots,\n\tcomplete with their color palettes, even though it only ever uses the\n\tfirst one of these. There goes another 458 bytes of conventional RAM… I\n\tshould start summing up all the waste we've seen so far. Let's put the\n\tnext website contribution towards a tagging system for these blog posts.\n\u003c/p\u003e\u003cp\u003e\n\tAt 86% of technical debt in the \u003ccode\u003eSHARED\u003c/code\u003e segment repaid, we\n\taren't quite done yet, but the rest is mostly just TH04 needing to catch\n\tup with functions we've already separated. Next up: Getting to that\n\tpractical 98.5% point. Since this is very likely to not require a full\n\tpush, I'll also decompile some more actual TH04 and TH05 game code I\n\tpreviously reverse-engineered – and after that, reopen the store!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-05-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-04-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-04-23T00:46:42Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-04-04",
      "url": "https://rec98.nmlgc.net/blog/2021-04-04",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-04-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-03-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-04-04\"\u003e\u003ctime datetime=\"2021-04-04T18:46:29Z\"\u003e2021-04-04 18:46\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0137\"\u003eP0137\u003c/a\u003e\n\t\t\tSeparating translation units, part 8/10 (focused around TH03) + Segment alignment research\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/07bfcf2...8d953dc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWhoops, the build was broken \u003ci\u003eagain\u003c/i\u003e? Since\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/7897bf1\"\u003eP0127 from\n\tmid-November 2020\u003c/a\u003e, on TASM32 version 5.3, which also happens to be the\n\tone in the DevKit… That version changed the alignment for the default\n\tsegments of certain memory models when requesting \u003ccode\u003e.386\u003c/code\u003e\n\tsupport. And since redefining segment alignment apparently is highly\n\tillegal and absolutely has to be a build error, some of the stand-alone\n\t.ASM translation units didn't assemble anymore on this version. I've only\n\tspotted this on my own because I casually compiled ReC98 somewhere else –\n\ton my development system, I happened to have TASM32 version 5.0 in the\n\t\u003ccode\u003ePATH\u003c/code\u003e during all this time.\u003cbr\u003e\n\tAt least this was a good occasion to\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/8bcf5d7\"\u003eget rid of some\n\tweird segment alignment workarounds from 2015, and replace them with the\n\tsuperior convention of using the \u003ccode\u003eUSE16\u003c/code\u003e modifier for the\n\t\u003ccode\u003e.MODEL\u003c/code\u003e directive.\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tReC98 would highly benefit from a build server – both in order to\n\timmediately spot issues like this one, and as a service for modders.\n\tEven more so than the usual open-source project of its size, I would say.\n\tBut that might be exactly\n\t\u003ci\u003ebecause\u003c/i\u003e it doesn't seem like something you can trivially outsource\n\tto one of the big CI providers for open-source projects, and quickly set\n\tit up with a few lines of YAML.\u003cbr\u003e\n\tThat might still work in the beginning, and we might get by with a regular\n\t64-bit Windows 10 and DOSBox running the exact build tools from the DevKit.\n\tIdeally, though, such a server should really run the optimal configuration\n\tof a 32-bit Windows 10, allowing both the 32-bit and the 16-bit build step\n\tto run natively, which already is something that no popular CI service out\n\tthere offers. Then, we'd optimally expand to Linux, every other Windows\n\tversion down to 95, emulated PC-98 systems, other TASM versions… yeah, it'd\n\tbe a lot. An experimental project all on its own, with additional hosting\n\tcosts and probably diminishing returns, the more it expands…\u003cbr\u003e\n\tI've added it as a category to the order form, let's see how much interest\n\tthere is once the store reopens (which will be at the beginning of May, at\n\tthe latest). That aside, it would \u003ca href=\"/blog/2020-07-12\"\u003e📝 also\u003c/a\u003e be\n\ta great project for outside contributors!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, technical debt, part 8… and right away, we're faced with TH03's\n\tlow-level input function, which\n\t\u003ca href=\"/blog/2020-11-16\"\u003e📝 once\u003c/a\u003e\n\t\u003ca href=\"/blog/2021-01-06\"\u003e📝 again\u003c/a\u003e\n\t\u003ca href=\"/blog/2021-01-31\"\u003e📝 insists\u003c/a\u003e on being word-aligned in a way we\n\tcan't fake without duplicating translation units.\n\tBeing undecompilable isn't exactly the best property for a function that\n\thas been interesting to modders in the past: In 2018,\n\t\u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e created an\n\tASM-level mod that hardcoded more ergonomic key bindings for human-vs-human\n\tmultiplayer mode: \u003ca class=\"download\" href=\"/blog/static/2021-04-04-TH03-WASD-2player.zip?d304a6d0\" data-kb=\"1109.5\"\u003e2021-04-04-TH03-WASD-2player.zip \u003c/a\u003e\n\tHowever, this remapping attempt remained quite limited, since we hadn't\n\t(and still haven't) reached full position independence for TH03 yet.\n\tThere's quite some potential for size optimizations in this function, which\n\twould allow more BIOS key groups to already be used right now, but it's not\n\tall that obvious to modders who aren't intimately familiar with x86 ASM.\n\tTherefore, I \u003ci\u003ereally\u003c/i\u003e wouldn't want to keep such a long and important\n\tfunction in ASM if we don't \u003ci\u003eabsolutely\u003c/i\u003e have to…\n\u003c/p\u003e\u003cp\u003e\n\t… and apparently, that's all the motivation I needed? So I took the risk,\n\tand spent the first half of this push on reverse-engineering\n\t\u003ccode\u003eTCC.EXE\u003c/code\u003e, to hopefully find a way to get word-aligned code\n\tsegments out of Turbo C++ after all.\n\u003c/p\u003e\u003cp\u003e\n\tAnd there is! The \u003ccode\u003e-WX\u003c/code\u003e option, used for creating\n\t\u003ca href=\"https://en.wikipedia.org/wiki/DOS_Protected_Mode_Interface\"\u003eDPMI\n\t\u003c/a\u003e applications, messes up all sorts of code generation aspects in weird\n\tways, but does in fact mark the code segment as word-aligned. We can\n\tconsider ourselves quite lucky that we get to use Turbo C++ 4.0, because\n\tthis feature isn't available in any previous version of Borland's C++\n\tcompilers.\u003cbr\u003e\n\tThat allowed us to restore all the decompilations I previously threw away…\n\twell, two of the three, that lookup table generator was too much of a mess\n\tin C. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e But \u003ci\u003ewhat\u003c/i\u003e an abuse this is. The\n\tsubtly different code generation has basically required one creative\n\tworkaround per usage of \u003ccode\u003e-WX\u003c/code\u003e. For example, enabling that option\n\tcauses the regular \u003ccode\u003ePUSH BP\u003c/code\u003e and \u003ccode\u003ePOP BP\u003c/code\u003e prolog and\n\tepilog instructions to be wrapped with \u003ccode\u003eINC BP\u003c/code\u003e and\n\t\u003ccode\u003eDEC BP\u003c/code\u003e, for some reason:\n\u003c/p\u003e\u003cpre\u003ea_function_compiled_with_wx proc\n\tinc \tbp    \t; ???\n\tpush\tbp\n\tmov \tbp, sp\n\t    \t      \t; [… function code …]\n\tpop \tbp\n\tdec \tbp    \t; ???\n\tret\na_function_compiled_with_wx endp\u003c/pre\u003e\u003cp\u003e\n\u003c/p\u003e\u003cp\u003e\n\tLuckily again, all the functions that currently require \u003ccode\u003e-WX\u003c/code\u003e\n\tdon't set up a stack frame and don't take any parameters.\u003cbr\u003e\n\tWhile this hasn't \u003ci\u003edirectly\u003c/i\u003e been an issue so far, it's been pretty\n\tclose: \u003ccode\u003esnd_se_reset(void)\u003c/code\u003e is one of the functions that require\n\tword alignment. Previously, it shared a translation unit with the\n\timmediately following \u003ccode\u003esnd_se_play(int new_se)\u003c/code\u003e, which does take\n\ta parameter, and therefore would have had its prolog and epilog code messed\n\tup by \u003ccode\u003e-WX\u003c/code\u003e.\n\tSince the latter function has a consistent (and thus, fakeable) alignment,\n\tI simply split that code segment into two, with a new \u003ccode\u003e-WX\u003c/code\u003e\n\ttranslation unit for just \u003ccode\u003esnd_se_reset(void)\u003c/code\u003e. Problem solved –\n\tafter all, two C++ translation units are still better than one ASM\n\ttranslation unit. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Especially with all the\n\tprevious \u003ccode\u003e#include\u003c/code\u003e improvements.\n\u003c/p\u003e\u003cp\u003e\n\tThe rest was more of the usual, getting us 74% done with repaying the\n\ttechnical debt in the \u003ccode\u003eSHARED\u003c/code\u003e segment. A lot of the remaining\n\t26% is TH04 needing to catch up with TH03 and TH05, which takes\n\tcomparatively little time. With some good luck, we \u003ci\u003emight\u003c/i\u003e get this\n\tdone within the next push… that is, if we aren't confronted with all too\n\tmany more disgusting decompilations, like the two functions that ended this\n\tpush.\n\tIf we are, we might be needing 10 pushes to complete this after all, but\n\tthat piece of research was definitely worth the delay. Next up: One more of\n\tthese.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-04-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-03-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-04-04T18:46:29Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-03-20",
      "url": "https://rec98.nmlgc.net/blog/2021-03-20",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-04-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-02-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-03-20\"\u003e\u003ctime datetime=\"2021-03-20T20:19:56Z\"\u003e2021-03-20 20:19\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0135\"\u003eP0135\u003c/a\u003e\n\t\t\tSeparating translation units, part 6/10 (TH05 PMD loading / Music Room piano)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/a6eed55...252c13d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0136\"\u003eP0136\u003c/a\u003e\n\t\t\tSeparating translation units, part 7/10 (starting to catch up with TH04)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/252c13d...07bfcf2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kaja\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The PMD and MMD sound drivers by Masahiro Kajihara (梶原 正裕).\"\u003ekaja\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAlright, no more big code maintenance tasks that absolutely need to be\n\tdone right now. Time to \u003ci\u003ereally\u003c/i\u003e focus on parts 6 and 7 of repaying\n\ttechnical debt, right? Except that we don't get to speed up just yet, as\n\tTH05's barely decompilable PMD file loading function is rather…\n\tcomplicated.\u003cbr\u003e\n\tFun fact: Whenever I see an unusual sequence of x86 instructions in PC-98\n\tTouhou, I first consult the disassembly of Wolfenstein 3D. That game was\n\toriginally compiled with the quite similar Borland C++ 3.0, so it's quite\n\thelpful to compare its ASM to the\n\t\u003ca href=\"https://github.com/id-Software/wolf3d\"\u003eofficially released source\n\tcode\u003c/a\u003e. If I find the instructions in question, they mostly come from\n\tthat game's ASM code, leading to the amusing realization that \"even John\n\tCarmack was unable to get these instructions out of this compiler\"\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e This time though, Wolfenstein 3D did point me\n\tto Borland's intrinsics for common C functions like \u003ccode\u003ememcpy()\u003c/code\u003e\n\tand \u003ccode\u003estrchr()\u003c/code\u003e, available via \u003ccode\u003e#pragma intrinsic\u003c/code\u003e.\n\tBu~t those unfortunately still generate worse code than what ZUN\n\tmicro-optimized here. Commenting how these sequences of instructions\n\t\u003ci\u003eshould\u003c/i\u003e look in C is unfortunately all I could do here.\u003cbr\u003e\n\tThe conditional branches in this function \u003ci\u003edid\u003c/i\u003e compile quite nicely\n\tthough, clarifying the control flow, \u003ci\u003eand\u003c/i\u003e clearly exposing a ZUN\n\tbug: TH05's \u003ccode\u003esnd_load()\u003c/code\u003e will hang in an infinite loop when\n\ttrying to load a non-existing -86 BGM file (with a \u003ccode\u003e.M2\u003c/code\u003e\n\textension) if the corresponding -26 BGM file (with a \u003ccode\u003e.M\u003c/code\u003e\n\textension) doesn't exist either.\n\u003c/p\u003e\u003cp\u003e\n\tUnsurprisingly, the PMD channel monitoring code in TH05's Music Room\n\tremains undecompilable outside the two most \"high-level\" initialization\n\tand rendering functions. And it's \u003ci\u003enot\u003c/i\u003e because there's data in the\n\tmiddle of the code segment – that would have actually been possible with\n\tsome \u003ccode\u003e#pragma\u003c/code\u003es to ensure that the data and code segments have\n\tthe same name. As soon as the SI and DI registers are referenced\n\t\u003ci\u003eanywhere\u003c/i\u003e, Turbo C++ insists on emitting prolog code to save these\n\ton the stack at the beginning of the function, and epilog code to restore\n\tthem from there before returning.\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/7f971a0\"\u003eFound that out in\n\tSeptember 2019, and confirmed that there's no way around it.\u003c/a\u003e All the\n\tsmall helper functions here are quite simply too optimized, throwing away\n\tany concern for such safety measures. 🤷\u003cbr\u003e\n\tOh well, the two functions that \u003ci\u003ewere\u003c/i\u003e decompilable at least indicate\n\tthat I do try.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWithin that same 6th push though, we've finally reached the one function\n\tin TH05 that was blocking further progress in TH04, allowing that game\n\tto finally catch up with the others in terms of separated translation\n\tunits. Feels good to finally delete more of those .ASM files we've\n\tdecompiled a while ago… finally!\n\u003c/p\u003e\u003cp\u003e\n\tBut since that was just getting started, the most satisfying development\n\tin both of these pushes actually came from some more experiments with\n\tmacros and \u003ccode\u003einline\u003c/code\u003e functions for near-ASM code. By adding\n\t\"unused\" dummy parameters for all relevant registers, the exact input\n\tregisters are made more explicit, which might help future port authors who\n\tthen \u003ci\u003emaybe\u003c/i\u003e wouldn't have to look them up in an x86 instruction\n\treference \u003ci\u003equite\u003c/i\u003e as often. At its best, this even allows us to\n\tdeclare certain functions with the \u003ccode\u003e__fastcall\u003c/code\u003e convention and\n\texpress their parameter lists as regular C, with no additional\n\tpseudo-registers or macros required.\u003cbr\u003e\n\tAs for output registers, Turbo C++'s code generation turns out to be even\n\tmore amazing than previously thought when it comes to returning\n\tpseudo-registers from \u003ccode\u003einline\u003c/code\u003e functions. A nice example for\n\thow this can improve readability can be found in this piece of TH02 code\n\tfor polling the PC-98 keyboard state using a BIOS interrupt:\n\u003c/p\u003e\u003cpre\u003einline uint8_t keygroup_sense(uint8_t group) {\n\t_AL = group;\n\t_AH = 0x04;\n\tgeninterrupt(0x18);\n\t// This turns the output register of this BIOS call into the return value\n\t// of this function. Surprisingly enough, this does *not* naively generate\n\t// the `MOV AL, AH` instruction you might expect here!\n\treturn _AH;\n}\n\nvoid input_sense(void)\n{\n\t// As a result, this assignment becomes `_AH = _AH`, which Turbo C++\n\t// never emits as such, giving us only the three instructions we need.\n\t_AH = keygroup_sense(8);\n\n\t// Whereas this one gives us the one additional `MOV BH, AH` instruction\n\t// we'd expect, and nothing more.\n\t_BH = keygroup_sense(7);\n\n\t// And now it's obvious what both of these registers contain, from just\n\t// the assignments above.\n\tif(_BH \u0026 K7_ARROW_UP || _AH \u0026 K8_NUM_8) {\n\t\tkey_det |= INPUT_UP;\n\t}\n\t// […]\n}\u003c/pre\u003e\u003cp\u003e\n\tI love it. No inline assembly, as close to idiomatic C code as something\n\tlike this is going to get, yet still compiling into the minimum possible\n\tnumber of x86 instructions on even a 1994 compiler. This is how I keep\n\tthis project interesting for myself during chores like these.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e We might have even reached peak\n\t\u003ccode\u003einline\u003c/code\u003e already?\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's 65% of technical debt in the \u003ccode\u003eSHARED\u003c/code\u003e segment repaid\n\tso far. Next up: Two more of these, which might already complete that\n\tsegment? Finally!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-04-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-02-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-03-20T20:19:56Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-02-21",
      "url": "https://rec98.nmlgc.net/blog/2021-02-21",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-03-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-01-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-02-21\"\u003e\u003ctime datetime=\"2021-02-21T13:36:38Z\"\u003e2021-02-21 13:36\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0134\"\u003eP0134\u003c/a\u003e\n\t\t\tSeparating translation units, part 5/10 (TH05 .PI functions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1d5db71...a6eed55\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTechnical debt, part 5… and we \u003ci\u003eonly\u003c/i\u003e got TH05's stupidly optimized\n\t.PI functions this time?\n\u003c/p\u003e\u003cp\u003e\n\tAs far as actual progress is concerned, that is. In maintenance news\n\tthough, I was really hyped for the \u003ccode\u003e#include\u003c/code\u003e improvements I've\n\tmentioned in \u003ca href=\"/blog/2021-01-31\"\u003e📝 the last post\u003c/a\u003e. The result: A\n\tnew \u003ccode\u003ex86real.h\u003c/code\u003e file, bundling all the declarations specific to\n\tthe 16-bit x86 Real Mode in a smaller file than Turbo C++'s own\n\t\u003ccode\u003eDOS.H\u003c/code\u003e. After all, DOS is something else than the underlying\n\tCPU. And while it didn't speed up build times quite as much as I had hoped,\n\tit now clearly indicates the x86-specific parts of PC-98 Touhou code to\n\tfuture port authors.\n\u003c/p\u003e\u003cp\u003e\n\tAfter another couple of improvements to parameter declaration in ASM land,\n\twe get to TH05's .PI functions… and really, why did ZUN write \u003ci\u003eall of\n\tthem\u003c/i\u003e in ASM? Why (re)declare all the necessary structures and data in\n\tASM land, when all these functions are merely one layer of abstraction\n\tabove master.lib, which does all the actual work?\u003cbr\u003e\n\tI get that ZUN might have wanted masked blitting to be faster, which is\n\tused for the fade-in effect seen during TH05's main menu animation and the\n\tending artwork. But, uh… he knew how to modify master.lib. In fact, he\n\t\u003ci\u003edid\u003c/i\u003e already modify the \u003ccode\u003egraph_pack_put_8()\u003c/code\u003e function\n\tused for rendering a single .PI image row, to ignore master.lib's VRAM\n\tclipping region. For this effect though, he first blits each row regularly\n\tto the invisible 400th row of VRAM, and \u003ci\u003ethen\u003c/i\u003e does an EGC-accelerated\n\tVRAM-to-VRAM blit of that row to its actual target position with the mask\n\tenabled. It would have been way more efficient to add another version of\n\tthis function that takes a mask pattern. No amount of \u003ccode\u003eREP\n\tMOVSW\u003c/code\u003e is going to change the fact that two VRAM writes per line are\n\tslower than a single one. Not to mention that it doesn't justify writing\n\tevery other .PI function in ASM to go along with it…\u003cbr\u003e\n\tThis is where we also find the most hilarious aspect about this: For most\n\tof ZUN's pointless micro-optimizations, you could have maybe made the\n\targument that they do save \u003ci\u003esome\u003c/i\u003e CPU cycles here and there, and\n\ttherefore did something positive to the final, PC-98-exclusive result. But\n\tsome of the hand-written ASM here doesn't even constitute a\n\tmicro-optimization, because it's \u003ci\u003eworse\u003c/i\u003e than what you would have got\n\tout of even Turbo C++ 4.0J with its 80386 optimization flags!\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tAt least it was \u003ci\u003epossible\u003c/i\u003e to \"decompile\" 6 out of the 10 functions\n\there, making them easy to clean up for future modders and port authors.\n\tCould have been 7 functions if I also decided to \"decompile\"\n\t\u003ccode\u003epi_free()\u003c/code\u003e, but all the C++ code is already surrounded by ASM,\n\tresulting in 2 ASM translation units and 2 C++ translation units.\n\t\u003ccode\u003epi_free()\u003c/code\u003e would have needed a single translation unit by\n\titself, which wasn't worth it, given that I would have had to spell out\n\tevery single ASM instruction anyway.\n\u003c/p\u003e\u003cpre\u003evoid pascal pi_free(int slot)\n{\n\tif(pi_buffers[slot]) {\n\t\tgraph_pi_free(\u0026pi_headers[slot], \u0026pi_buffers[slot]);\n\t\tpi_buffers[slot] = NULL;\n\t}\n}\u003c/pre\u003e\u003cp\u003e\n\tThere you go. What about this needed to be written in ASM?!?\n\u003c/p\u003e\u003cp\u003e\n\tThe function calls between these small translation units even seemed to\n\tglitch out TASM and the linker in the end, leading to one \u003ccode\u003eCALL\u003c/code\u003e\n\toffset being weirdly shifted by 32 bytes. Usually, TLINK reports a fixup\n\toverflow error when this happens, but this time it didn't, for some reason?\n\tMirroring the segment grouping in the affected translation unit did solve\n\tthe problem, and I already knew this, but only thought of it after spending\n\tquite some RTFM time… during which I discovered the \u003ccode\u003e-lE\u003c/code\u003e\n\tswitch, which enables TLINK to use the \u003ci\u003eexpanded dictionaries\u003c/i\u003e in\n\tBorland's .OBJ and .LIB files to speed up linking. That shaved off roughly\n\tanother second from the build time of the complete ReC98 repository. The\n\tmore you know… Binary blobs compiled with non-Borland tools would be the\n\tonly reason not to use this flag.\n\u003c/p\u003e\u003cp\u003e\n\tSo, even more slowdown with this 5th dedicated push, since we've still only\n\trepaid 41% of the technical debt in the \u003ccode\u003eSHARED\u003c/code\u003e segment so far.\n\tNext up: Part 6, which hopefully manages to decompile the FM and SSG\n\tchannel animations in TH05's Music Room, and hopefully ends up being the\n\tfinal one of the slow ones.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-03-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-01-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-02-21T13:36:38Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-01-31",
      "url": "https://rec98.nmlgc.net/blog/2021-01-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-02-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-01-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-01-31\"\u003e\u003ctime datetime=\"2021-01-31T15:54:43Z\"\u003e2021-01-31 15:54\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0133\"\u003eP0133\u003c/a\u003e\n\t\t\tSeparating translation units, part 4/10 (focused around TH02 / TH05)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/045450c...1d5db71\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWow, 31 commits in a single push? Well, what the last push had in\n\tprogress, this one had in maintenance. The\n\t\u003ca href=\"/blog/2020-11-03\"\u003e📝 master.lib header transition\u003c/a\u003e absolutely\n\t\u003ci\u003ehad\u003c/i\u003e to be completed in this one, for my own sanity. And indeed,\n\tit reduced the build time for the entirety of ReC98 to about 27 seconds on\n\tmy system, just as expected in the original announcement. Looking forward\n\tto even faster build times with the upcoming \u003ccode\u003e#include\u003c/code\u003e\n\timprovements I've got up my sleeve! The port authors of the future are\n\tgoing to appreciate those quite a bit.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the new translation units, the funniest one is probably TH05's\n\tfunction for blitting the 1-color .CDG images used for the main menu\n\toptions. Which is \u003ci\u003eso\u003c/i\u003e optimized that it becomes decompilable again,\n\tby ditching the self-modifying code of its TH04 counterpart in favor of\n\tsimply making better use of CPU registers. The resulting C code is still a\n\tmess, but what can you do. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tThis was followed by even more TH05 functions that clearly weren't\n\tcompiled from C, as evidenced by their \u003ca href=\"https://github.com/nmlgc/ReC98/blob/1d5db7155aa31467c9256f58112420f079078f81/Research/Borland%20C%2B%2B%20decompilation.md#padding-bytes-in-code-segments\"\u003epadding\n\tbytes\u003c/a\u003e. It's about time I've documented my lack of ideas of how to get\n\tthose out of Turbo C++. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tAnd just like in the previous push, I also had to \u003ca href=\"/blog/2020-11-16\"\u003e📝 throw away\u003c/a\u003e a decompiled TH02 function purely due to alignment issues. Couldn't have been a better one though, no one's going to miss a residency check for the MMD driver that is largely identical to the corresponding (and indeed decompilable) function for the PMD driver. Both of those should have been merged into a single function anyway, given how they also mutate the game's sound configuration flags…\n\u003c/p\u003e\u003cp\u003e\n\tIn the end, I've slightly slowed down with this one, with \u003ci\u003eonly\u003c/i\u003e 37% of technical debt done after this 4th dedicated push. Next up: One more of these, centered around TH05's stupidly optimized .PI functions. Maybe also with some more reverse-engineering, after not having done any for 1½ months?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-02-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2021-01-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-01-31T15:54:43Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2021-01-06",
      "url": "https://rec98.nmlgc.net/blog/2021-01-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-01-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-12-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2021-01-06\"\u003e\u003ctime datetime=\"2021-01-06T17:29:14Z\"\u003e2021-01-06 17:29\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0132\"\u003eP0132\u003c/a\u003e\n\t\t\tSeparating translation units, part 3/10 (focused around TH02 / TH03)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dc9e3ee...045450c\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNow \u003ci\u003ethat's\u003c/i\u003e the amount of translation unit separation progress I was\n\tlooking for! Too bad that RL is keeping me more and more occupied these\n\tdays, and ended up delaying this push until 2021. Now that\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e is also commissioning me to update their\n\tinfrastructure, it's going to take a while for ReC98 to return to full\n\tspeed, and for the store to be reopened. Should happen by April at the\n\tlatest, though!\n\u003c/p\u003e\u003cp\u003e\n\tWith everything related to this separation of translation units explained\n\tearlier, we've \u003ci\u003ereally\u003c/i\u003e got a push with nothing to talk about, this\n\ttime. Except, maybe, for the realization that\n\t\u003ca href=\"/blog/2020-09-07\"\u003e📝 this current approach\u003c/a\u003e might not be the\n\tbest fit for TH02 after all: Not only did it force us to\n\t\u003ca href=\"/blog/2020-11-16\"\u003e📝 throw away the previous decompilation\u003c/a\u003e of\n\tthe sound effect playback functions, but \u003ccode\u003eOP.EXE\u003c/code\u003e also contains\n\tobviously copy-pasted code \u003ci\u003ein addition\u003c/i\u003e to the common, shared set of\n\tlibrary functions. How was that game even \u003ci\u003ebuilt\u003c/i\u003e, originally??? No\n\tway around compiling that one instance of the \u003ci\u003e\"delay until given BGM\n\tmeasure\"\u003c/i\u003e function separately then, if it insists on using its own\n\tinstance of the VSync delay function…\u003cbr\u003e\n\tOh well, this separated layout still works better for the later games, and\n\tconsistency is good. Smooth sailing with all of the other functions, at\n\tleast.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: One more of these, which might even end up completing the\n\t\u003ca href=\"/blog/2020-11-03\"\u003e📝 transition to our own master.lib header file\u003c/a\u003e.\n\tIn terms of the total number of ASM code left in the \u003ccode\u003eSHARED\u003c/code\u003e\n\tcode segments, we're now 30% done after 3 dedicated pushes. It really\n\tshouldn't require 7 more pushes, though!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-01-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-12-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2021-01-06T17:29:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-12-18",
      "url": "https://rec98.nmlgc.net/blog/2020-12-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-01-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-12-18\"\u003e\u003ctime datetime=\"2020-12-18T17:43:32Z\"\u003e2020-12-18 17:43\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0130\"\u003eP0130\u003c/a\u003e\n\t\t\tTH01 decompilation (Boss HP and collision handling, part 1/2) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6d69ea8...576def5\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0131\"\u003eP0131\u003c/a\u003e\n\t\t\tTH01 decompilation (Boss HP and collision handling, part 2/2) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/576def5...dc9e3ee\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/good-code\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rng\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pseudo-randomness, in a gameplay sense. Emphasis on \u0026#34;pseudo\u0026#34;.\"\u003erng\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t50% hype! 🎉 But as usual for TH01, even that final set of functions\n\tshared between all bosses had to consume two pushes rather than one…\n\u003c/p\u003e\u003cp\u003e\n\tFirst up, in the ongoing series \"Things that TH01 draws to the PC-98\n\tgraphics layer that really should have been drawn to the text layer\n\tinstead\": The boss HP bar. Oh well, using the graphics layer at least made\n\tit possible to have this half-red, half-white pattern\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAPCAIAAACnY0LpAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYSURBVChTY/jPwPAfCFDJUVEISbwoAwMAdbLCPuvaYX8AAAAASUVORK5CYII=\" alt=\"\"\u003e\n\tfor the middle section.\u003cbr\u003e\n\tThis one pattern is drawn by making surprisingly good use of the GRCG. So\n\tfar, we've only seen it used for fast monochrome drawing:\n\u003c/p\u003e\u003cpre\u003e// Setting up fast drawing using color #9 (1001 in binary)\ngrcg_setmode(GC_RMW);\noutportb(0x7E, 0xFF); // Plane 0: (B): (********)\noutportb(0x7E, 0x00); // Plane 1: (R): (        )\noutportb(0x7E, 0x00); // Plane 2: (G): (        )\noutportb(0x7E, 0xFF); // Plane 3: (E): (********)\n\n// Write a checkerboard pattern (* * * * ) in color #9 to the top-left corner,\n// with transparent blanks. Requires only 1 VRAM write to a single bitplane:\n// The GRCG automatically writes to the correct bitplanes, as specified above\n*(uint8_t *)(MK_FP(0xA800, 0)) = 0xAA;\u003c/pre\u003e\u003cp\u003e\n\tBut since this is actually an 8-pixel \u003ci\u003etile register\u003c/i\u003e, we can set any\n\t8-pixel pattern for any bitplane. This way, we can get different colors\n\tfor every one of the 8 pixels, with still just a single VRAM write of the\n\talpha mask to a single bitplane:\n\u003c/p\u003e\u003cpre\u003egrcg_setmode(GC_RMW); //  Final color: (A7A7A7A7)\noutportb(0x7E, 0x55); // Plane 0: (B): ( * * * *)\noutportb(0x7E, 0xFF); // Plane 1: (R): (********)\noutportb(0x7E, 0x55); // Plane 2: (G): ( * * * *)\noutportb(0x7E, 0xAA); // Plane 3: (E): (* * * * )\u003c/pre\u003e\u003cp\u003e\n\tAnd I thought TH01 only suffered the drawbacks of PC-98 hardware, making\n\tso little use of its actual features that it's perhaps not fair to even\n\tcall it \"a PC-98 game\"… Still, I'd say that \"bad PC-98 port of an idea\"\n\tdescribes it best.\n\u003c/p\u003e\u003cp\u003e\n\tHowever, after that tiny flash of brilliance, the surrounding HP rendering\n\tcode goes right back to being the typical sort of confusing TH01 jank.\n\tThere's only a single function for the three distinct jobs of\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eincrementing HP during the boss entrance animation,\u003c/li\u003e\n\t\u003cli\u003edecrementing HP if hit by the Orb, and\u003c/li\u003e\n\t\u003cli\u003eredrawing the entire bar, because it's still all in VRAM, and Sariel\n\twants different backgrounds,\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\twith magic numbers to select between all of these.\n\u003c/p\u003e\u003cp\u003e\n\tVRAM of course also means that the backgrounds behind the individual hit\n\tpoints have to be stored, so that they can be unblitted later as the boss\n\tis losing HP. That's no big deal though, right? Just allocate some memory,\n\tcopy what's initially in VRAM, then blit it back later using your\n\tfoundational set of blitting funct– oh, wait, TH01 doesn't have this sort\n\tof thing, right \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e The closest thing,\n\t\u003ca href=\"/blog/2020-07-27\"\u003e📝 once again\u003c/a\u003e, are the .PTN functions. And\n\tso, the game ends up handling these 8×16 background sprites with 16×16\n\twrappers around functions for 32×32 sprites. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\tThat's quite the recipe for confusion, \u003ci\u003eespecially\u003c/i\u003e since ZUN\n\tpreferred copy-pasting the necessary ridiculous arithmetic expressions for\n\tcalculating positions, .PTN sprite IDs, and the ID of the 16×16 quarter\n\tinside the 32×32 sprite, instead of just writing simple helper functions.\n\t\u003cs\u003eHe \u003ci\u003edid\u003c/i\u003e manage to make the result \u003ci\u003emostly\u003c/i\u003e bug-free this time\n\taround, though!\u003c/s\u003e (\u003cstrong\u003eEdit (2022-05-31):\u003c/strong\u003e Nope, there's a\n\t\u003ca href=\"/blog/2022-05-31\"\u003e📝 potential heap corruption after all, which can be triggered in some fights in test mode (\u003ccode\u003egame t\u003c/code\u003e) or debug mode (\u003ccode\u003egame d\u003c/code\u003e)\u003c/a\u003e.)\n\tThere's one minor hit point discoloration bug if the red-white or white\n\tsections start at an odd number of hit points, but that's never the case for\n\tany of the original 7 bosses.\u003cbr\u003e\n\tThe remaining sloppiness is ultimately inconsequential as well: The game\n\talways backs up \u003ci\u003etwice\u003c/i\u003e the number of hit point backgrounds, and thus\n\tuses twice the amount of memory actually required. Also, this\n\tself-restriction of only unblitting 16×16 pixels at a time requires any\n\tremaining odd hit point at the last position to, of course, be rendered\n\tagain \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter stumbling over the weakest imaginable random number\n\t\u003cs\u003egenerator\u003c/s\u003e, we finally arrive at the shared boss↔orb collision\n\thandling function, the final blocker among the final blockers. This\n\tfunction takes a whopping 12 parameters, 3 of them being references to\n\t\u003ccode\u003eint\u003c/code\u003e values, some of which are duplicated for every one of the\n\t7 bosses, with no generic boss \u003ccode\u003estruct\u003c/code\u003e anywhere.\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 Previously, I speculated that YuugenMagan might have been the first boss to be programmed for TH01\u003c/a\u003e.\n\tWith all these variables though, there is some new evidence that SinGyoku\n\tmight have been the first one after all: It's the only boss to use its own\n\tHP and phase frame variables, with the other bosses sharing the same two\n\tglobals.\n\u003c/p\u003e\u003cp\u003e\n\tWhile this function only handles the \u003ci\u003eresponse\u003c/i\u003e to a boss↔orb\n\tcollision, it still does way too much to describe it briefly. Took me\n\tquite a while to frame it in terms of \u003ci\u003einvincibility\u003c/i\u003e (which is the\n\tmain impact of all of this that can be observed in gameplay code). That\n\tmade at least \u003ci\u003esome\u003c/i\u003e sort of sense, considering the other usages of\n\tthe variables passed as references to that function. Turns out that\n\tYuugenMagan, Kikuri, and Elis abuse what's meant to be the \"invincibility\n\tframe\" variable as a frame counter for some of their animations 🙄\u003cbr\u003e\n\tOh well, the game at least doesn't call the collision handling function\n\tduring those, so \"invincibility frame\" is \u003ci\u003etechnically\u003c/i\u003e still a\n\tcorrect variable name there.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's it! We're finally ready to start with Konngara, in 2021. I've\n\tbeen waiting quite a while for this, as all this high-level boss code is\n\tvery likely to speed up TH01 progress quite a bit. Next up though: Closing\n\tout 2020 with more of the technical debt in the other games.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2021-01-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-12-18T17:43:32Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-11-30",
      "url": "https://rec98.nmlgc.net/blog/2020-11-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-12-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-11-30\"\u003e\u003ctime datetime=\"2020-11-30T23:48:26Z\"\u003e2020-11-30 23:48\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0128\"\u003eP0128\u003c/a\u003e\n\t\t\tTH01 decompilation (Card-flipping stages, part 1/4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dc65b59...dde36f7\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0129\"\u003eP0129\u003c/a\u003e\n\t\t\tTH01 decompilation (Card-flipping stages, part 2/4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dde36f7...f4c2e45\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/card-flipping\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/debug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Debug features that ZUN left in the shipped game.\"\u003edebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hidden-content\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Hidden features in the original games.\"\u003ehidden-content\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, only one card-flipping function missing, and then we can start\n\tdecompiling TH01's two final bosses? Unfortunately, that had to be the one\n\tbig function that initializes and renders all gameplay objects. #17 on the\n\tlist of longest functions in all of PC-98 Touhou, requiring two pushes to\n\tfully understand what's going on there… \u003ci\u003eand then it immediately returns\n\tfor all \"boss\" stages whose number is divisible by 5, yet is still called\n\tduring Sariel's and Konngara's initialization\u003c/i\u003e 🤦\n\u003c/p\u003e\u003cp\u003e\n\tOh well. This also involved the final file format we hadn't looked at\n\tyet – the \u003ccode\u003eSTAGE?.DAT\u003c/code\u003e files that describe the layout for all\n\tstages within a single 5-stage scene. Which, for a change is a very\n\twell-designed form– no, of course it's completely weird, what did you\n\texpect? Development must have looked somewhat like this:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWeirdness #1: \u003ci\u003e\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e \"Hm, the stage format should\n\tinclude the file names for the background graphics and music… or should\n\tit?\"\u003c/i\u003e And so, the 22-byte header still references some music and\n\tbackground files that aren't part of the final game. The game doesn't use\n\t\u003ci\u003eanything\u003c/i\u003e from there, and instead derives those file names from the\n\tscene ID.\u003cbr\u003e\n\tThat's probably nothing new to anyone who has ever looked at TH01's data\n\tfiles. In a slightly more interesting discovery though, seeing the\n\t\u003ca href=\"/blog/2020-07-27\"\u003e📝 .GRF extension\u003c/a\u003e, in some of the file names\n\tthat are short enough to not cut it off, confirms that .GRF was initially\n\tused for background images. Probably before ZUN learned about .PI, and how\n\tit achieves better compression than his own per-bitplane RLE approach?\u003c/li\u003e\n\u003c/ul\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003eWeirdness #2: \u003ci\u003e\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e \"Hm, I might want to put\n\tobstacles on top of cards?\"\u003c/i\u003e You'd probably expect this format to\n\tcontain one single array for every stage, describing which object to place\n\ton every 32×32 tile, if any. Well, the real format uses \u003ci\u003etwo\u003c/i\u003e arrays:\n\tOne for the cards, and a combined one for all \"obstacles\" – bumpers, bumper\n\tbars, turrets, and portals. However, none of the card-flipping stages in\n\tthe final game come with any such overlaps. That's quite unfortunate, as it\n\twould have made for some quite interesting level designs:\u003c/p\u003e\n\t\u003cfigure\u003e\u003ca href=\"/blog/static/2020-11-30-TH01-Overlapping-obstacles-and-cards.png?311b89a8\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-11-30-TH01-Overlapping-obstacles-and-cards.png?311b89a8\"\n\t\talt=\"Experimenting with putting obstacles on top of cards in TH01\"\n\t\u003e\u003c/a\u003e\u003c/figure\u003e\n\t\u003cp\u003eAs you can see, the final version of the blitting code was not written\n\twith such overlaps in mind either, blitting the \u003ci\u003ecards\u003c/i\u003e on top of all\n\tthe \u003ci\u003eobstacles\u003c/i\u003e, and not the other way round.\u003c/p\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cul\u003e\u003cli\u003e\n\t\u003cp\u003eWeirdness #3: \u003ci\u003e\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e \"In contrast to obstacles, of\n\twhich there are multiple types, cards only really need 1 bit. Time for some\n\tbit twiddling!\"\u003c/i\u003e Not the worst idea, given that the 640×336 playfield\n\tcan fit 20×10 cards, which would fit exactly into 25 bytes if you use a\n\tsingle bit to indicate \u003ci\u003ecard\u003c/i\u003e or \u003ci\u003eno card\u003c/i\u003e. But for whatever\n\treason, ZUN only stored 4 card bits per byte, leaving the other 4 bits\n\tunused, and needlessly blowing up that array to 50 bytes. 🤷\u003c/p\u003e\n\t\u003cp\u003eOh, and did I mention that the contents of the STAGE?.DAT files are\n\tloaded into the main data segment, even though the game immediately parses\n\tthem into something more conveniently accessible? That's another 1250 bytes\n\tof memory wasted for no reason…\u003c/p\u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cul\u003e\n\t\u003cli\u003eWeirdness #4: \u003ci\u003e\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e \"Hm, how about requiring the\n\tplayer to flip some of the cards multiple times? But I've already written\n\tall this bit twiddling code to store 4 cards in 1 byte. And if cards should\n\tneed anywhere from 1 to 4 flips, that would need at least 2 more bits,\n\twhich won't fit into the unused 4 bits either…\"\u003c/i\u003e This feature\n\t\u003ci\u003emust\u003c/i\u003e have come later, because the final game uses 3 \"obstacle\" type\n\tIDs to act as a flip count modifier for a card at the same relative array\n\tposition. Complete with lookup code to find the actual card index these\n\tmodifiers belong to, and ridiculous \u003ci\u003eswitch\u003c/i\u003e statements to not include\n\tthose non-obstacles in the game's internal obstacle array.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/li\u003e\u003c/ul\u003e\u003cp\u003e\n\tWith all that, it's almost not worth mentioning how there are 12 turret\n\ttypes, which only differ in which hardcoded pellet group they fire at a\n\thardcoded interval of either 100 or 200 frames, and that they're all\n\texplicitly spelled out in every single \u003ccode\u003eswitch\u003c/code\u003e statement. Or\n\thow the layout of the internal card and obstacle SoA classes is quite\n\tdisjointed. So here's the new ZUN bugs you've probably already been\n\texpecting!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tCards and obstacles are blitted to both VRAM pages. This way, any other\n\tentities moving on top of them can simply be unblitted by restoring pixels\n\tfrom VRAM page 1, without requiring the stationary objects to be redrawn\n\tfrom main memory. Obviously, the backgrounds behind the cards have to be\n\tstored somewhere, since the player can remove them. For faster transitions\n\tbetween stages of a scene, ZUN chose to store the backgrounds behind\n\tobstacles as well. This way, the background image really only needs to be\n\tblitted for the first stage in a scene.\n\u003c/p\u003e\u003cp\u003e\n\tAll that memory for the object backgrounds adds up quite a bit though. ZUN\n\tactually made the correct choice here and picked a memory allocation\n\tfunction that can return more than the 64 KiB of a single x86 Real Mode\n\tsegment. He then accesses the individual backgrounds via regular array\n\tsubscripts… and that's where the bug lies, because he stores the returned\n\taddress in a regular \u003ccode\u003efar\u003c/code\u003e pointer rather than a\n\t\u003ccode\u003ehuge\u003c/code\u003e one. This way, the game \u003ci\u003estill\u003c/i\u003e can only display a\n\ttotal of 102 objects (i.\u0026nbsp;e., cards and obstacles combined) per stage,\n\twithout any unblitting glitches.\u003cbr\u003e\n\tWhat a shame, that limit could have been 127 if ZUN didn't needlessly\n\tallocate memory for \u003ci\u003ealpha planes\u003c/i\u003e when backing up VRAM content.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tAnd since array subscripts on \u003ccode\u003efar\u003c/code\u003e pointers wrap around after\n\t64 KiB, trying to save the background of the 103rd object is guaranteed to\n\tcorrupt the memory block header at the beginning of the returned segment.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e When TH01 runs in \u003cstrong\u003ed\u003c/strong\u003eebug mode, it\n\tcorrectly reports a corrupted heap in this case.\u003cbr\u003e\n\tAfter detecting such a corruption, the game loudly reports it by playing the\n\t\"player hit\" sound effect and locking up, freezing any further gameplay or\n\trendering. The locking loop can be left by pressing ↵\u0026nbsp;Return, but the\n\tgame will simply re-enter it if the corruption is still present during the\n\tnext \u003ccode\u003eheapcheck()\u003c/code\u003e, in the next frame. And since heap\n\tcorruptions don't tend to repair themselves, you'd have to constantly hold\n\t↵\u0026nbsp;Return to resume gameplay. Doing that \u003ci\u003ecould\u003c/i\u003e actually get you\n\tsafely to the next boss, since the game doesn't allocate or free any further\n\theap memory during a 5-stage \u003cspan class=\"tag\"\u003e\u003ca href=\"/blog/tag/card-flipping\" title=\"TH01\u0026#39;s regular, non-boss stages.\"\u003ecard-flipping\u003c/a\u003e\u003c/span\u003e scene, and\n\tjust throws away its C heap when restarting the process for a boss. But then\n\tagain, holding ↵\u0026nbsp;Return will also auto-flip all cards on the way there…\n\t🤨\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tFinally, some unused content! Upon discovering TH01's stage selection debug\n\tfeature, probably everyone tried to access Stage 21,\n\tjust to see what happens, and indeed landed in an actual stage, with a\n\tblack background and a weird color palette. Turns out that ZUN \u003ci\u003edid\u003c/i\u003e\n\tship an unused scene in \u003ccode\u003eSCENE7.DAT\u003c/code\u003e, which is exactly what's\n\tloaded there.\u003cbr\u003e\n\tHowever, it's easy to believe that this is just garbage data (as I\n\tinitially did): At the beginning of \"Stage 22\", the game seems to enter an\n\tinfinite loop somewhere during the flip-in animation.\n\u003c/p\u003e\u003cp\u003e\n\tWell, we've had a heap overflow above, and the cause here is nothing but a\n\tstack buffer overflow – a perhaps more \u003ci\u003emodern\u003c/i\u003e kind of classic C bug,\n\tgiven its prevalence in the Windows Touhou games. Explained in a few lines\n\tof code:\n\u003c/p\u003e\u003cpre\u003evoid stageobjs_init_and_render()\n{\n\tint card_animation_frames[50]; // even though there can be up to 200?!\n\tint total_frames = 0;\n\n\t(code that would end up resetting total_frames if it ever tried to reset\n\tcard_animation_frames[50]…)\n}\u003c/pre\u003e\u003cp\u003e\n\tThe number of cards in \"Stage 22\"? 76. There you have it.\n\u003c/p\u003e\u003cp\u003e\n\tBut of course, it's trivial to disable this animation and fix these stage\n\ttransitions. So here they are, Stages 21 to 24, as shipped with the game\n\tin \u003ccode\u003eSTAGE7.DAT\u003c/code\u003e:\n\u003c/p\u003e\n\u003cfigure class=\"side_by_side medium\"\u003e\u003ca href=\"/blog/static/2020-11-30-TH01-Stage-21.png?362169dc\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-11-30-TH01-Stage-21.png?362169dc\"\n\t\talt=\"TH01 stage 21, loaded from \u003ccode\u003eSTAGE7.DAT\u003c/code\u003e\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-11-30-TH01-Stage-22.png?40dff67e\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-11-30-TH01-Stage-22.png?40dff67e\"\n\t\talt=\"TH01 stage 22, loaded from \u003ccode\u003eSTAGE7.DAT\u003c/code\u003e\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-11-30-TH01-Stage-23.png?c97b5ac1\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-11-30-TH01-Stage-23.png?c97b5ac1\"\n\t\talt=\"TH01 stage 23, loaded from \u003ccode\u003eSTAGE7.DAT\u003c/code\u003e\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-11-30-TH01-Stage-24.png?c4751cb6\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-11-30-TH01-Stage-24.png?c4751cb6\"\n\t\talt=\"TH01 stage 24, loaded from \u003ccode\u003eSTAGE7.DAT\u003c/code\u003e\"\n\t\u003e\u003c/a\u003e\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tWow, what a mess. All that was just a bit too much to be covered in two\n\tpushes… Next up, assuming the current subscriptions: Taking a vacation with\n\tone smaller TH01 push, covering some smaller functions here and there to\n\tensure some uninterrupted Konngara progress later on.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-12-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-11-30T23:48:26Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-11-16",
      "url": "https://rec98.nmlgc.net/blog/2020-11-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-11-16\"\u003e\u003ctime datetime=\"2020-11-16T20:39:05Z\"\u003e2020-11-16 20:39\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0126\"\u003eP0126\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (EGC-powered blitting + .MRS format, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6c22af7...8b01657\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0127\"\u003eP0127\u003c/a\u003e\n\t\t\tTH03 decompilation (.MRS format, part 2/2) + separating translation units, part 2/10 \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8b01657...dc65b59\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAlright, back to continuing the \u003ccode\u003emaster.hpp\u003c/code\u003e transition started\n\tin P0124, and repaying technical debt. The last blog post already\n\tannounced some ridiculous decompilations… and in fact, \u003ci\u003enot a single\n\tone\u003c/i\u003e of the functions in these two pushes was decompilable into\n\tidiomatic C/C++ code.\n\u003c/p\u003e\u003cp\u003e\n\tAs usual, that didn't keep me from trying though. The TH04 and TH05\n\tversion of the infamous \u003ci\u003e16-pixel-aligned, EGC-accelerated rectangle\n\tblitting function from page 1 to page 0\u003c/i\u003e was fairly average as far as\n\tunreasonable decompilations are concerned.\u003cbr\u003e\n\tThe big blocker in TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e, however, turned out to be\n\tthe .MRS functions, used to render the gauge attack portraits and bomb\n\tbackgrounds. The blitting code there uses the additional FS and GS segment\n\tregisters provided by the Intel 386… which\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eare not supported by Turbo C++'s inline assembler, and\u003c/li\u003e\n\t\u003cli\u003ecan't be turned into pointers, due to a compiler bug in Turbo C++ that\n\tgenerates wrong segment prefix opcodes for the \u003ccode\u003e_FS\u003c/code\u003e and\n\t\u003ccode\u003e_GS\u003c/code\u003e pseudo-registers.\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tApparently I'm the first one to even try doing that with this compiler? I\n\thaven't found any other mention of this bug…\u003cbr\u003e\n\tCompiling via assembly (\u003ccode\u003e#pragma inline\u003c/code\u003e) would work around\n\tthis bug and generate the correct instructions. But that would incur yet\n\tanother dependency on a 16-bit TASM, for something honestly quite\n\tinsignificant.\n\u003c/p\u003e\u003cp\u003e\n\tWhat we can always do, however, is using \u003ccode\u003e__emit__()\u003c/code\u003e to simply\n\toutput x86 opcodes anywhere in a function. Unlike spelled-out inline\n\tassembly, that can even be used in helper functions that are supposed to\n\tinline… which does in fact allow us to fully abstract away this compiler\n\tbug. Regular \u003ccode\u003eif()\u003c/code\u003e comparisons with pseudo-registers\n\t\u003ci\u003ewouldn't\u003c/i\u003e inline, but \"converting\" them into C++ template function\n\tspecializations \u003ci\u003edoes\u003c/i\u003e. All that's left is some C preprocessor abuse\n\tto turn the pseudo-registers into types, and then we \u003ci\u003edo\u003c/i\u003e retain a\n\tnormal-looking \u003ccode\u003epoke()\u003c/code\u003e call in the blitting functions in the\n\tend. 🤯\n\u003c/p\u003e\u003cp\u003e\n\tYeah… the result is\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/8b0165738a1fd66\"\u003ebatshit\u003c/a\u003e\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/00e65f4c6b33eeb\"\u003einsane.\u003c/a\u003e\n\tI may have gone too far in a few places…\u003cbr\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOne might certainly argue that all these ridiculous decompilations\n\tactually hurt the preservation angle of this project. \u003ci\u003e\"Clearly, ZUN\n\tcouldn't have \u003cstrong\u003epossibly\u003c/strong\u003e written such unreasonable C++ code.\n\tSo why pretend he did, and not just keep it all in its more natural ASM\n\tform?\"\u003c/i\u003e Well, there are several reasons:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFuture port authors will merely have to translate all the\n\tpseudo-registers and inline assembly to C++. For the former, this is\n\ttypically as easy as replacing them with newly declared local variables. No\n\tneed to bother with function prolog and epilog code, calling conventions, or\n\tthe build system.\u003c/li\u003e\n\t\u003cli\u003eNo duplication of constants and structures in ASM land.\u003c/li\u003e\n\t\u003cli\u003eAs a more expressive language, C++ can document the code much better.\n\tMeticulous documentation seems to have become the main attraction of ReC98\n\tthese days – I've seen it appreciated quite a number of times, and the\n\tcontinued financial support of all the backers speaks volumes. Mods, on the\n\tother hand, are still a rather rare sight.\u003c/li\u003e\n\t\u003cli\u003eHaving as few .ASM files in the source tree as possible looks better to\n\tcasual visitors who just look at GitHub's repo language breakdown. This way,\n\tReC98 will also turn from an \u003ci\u003e\"Assembly project\"\u003c/i\u003e to its rightful state\n\tof \u003ci\u003e\"C++ project\"\u003c/i\u003e much sooner.\u003c/li\u003e\n\t\u003cli\u003eAnd finally, it's not like the ASM versions are\n\t\u003ci\u003egone\u003c/i\u003e\u0026nbsp;– they're still part of the Git history.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tUnfortunately, these pushes also demonstrated a second disadvantage in\n\ttrying to decompile everything possible: Since Turbo C++ lacks TASM's\n\tfine-grained ability to enforce code alignment on certain multiples of\n\tbytes, it might actually be unfeasible to link in a C-compiled object file\n\tat its intended original position in some of the .EXE files it's used in.\n\tWhich… you're only going to notice once you encounter such a case. Due to\n\tthe slightly jumbled order of functions in the\n\t\u003ca href=\"/blog/2020-09-07\"\u003e📝 second, shared code segment\u003c/a\u003e, that might\n\tbe long after you decompiled and successfully linked in the function\n\teverywhere else.\n\u003c/p\u003e\u003cp\u003e\n\tAnd then you'll have to throw away that decompilation after all 😕 Oh\n\twell. In this specific case (the lookup table generator for horizontally\n\tflipping images), that decompilation was a mess anyway, and probably\n\thelped nobody. I could have added a dummy .OBJ that does nothing but\n\tenforce the needed 2-byte alignment before the function if I\n\t\u003ci\u003ereally\u003c/i\u003e insisted on keeping the C version, but it really wasn't\n\tworth it.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNow that I've also described yet another meta-issue, maybe there'll\n\t\u003ci\u003ereally\u003c/i\u003e be nothing to say about the next technical debt pushes?\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e Next up though: Back to actual progress\n\tagain, with TH01. Which maybe even ends up pushing that game over the 50%\n\tRE mark?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-11-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-11-16T20:39:05Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-11-03",
      "url": "https://rec98.nmlgc.net/blog/2020-11-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-10-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-11-03\"\u003e\u003ctime datetime=\"2020-11-03T01:44:47Z\"\u003e2020-11-03 01:44\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0124\"\u003eP0124\u003c/a\u003e\n\t\t\tTH04 decompilation (Character selection, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/72dfa09...056b1c7\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0125\"\u003eP0125\u003c/a\u003e\n\t\t\tTH04 decompilation (Character selection, part 2/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/056b1c7...f6a3246\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eBlue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/master.lib\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A library providing an abstraction layer for all components of a PC-98 DOS system, collected and extended by Akihiko Koizuka (恋塚 昭彦). Written entirely in 16-bit x86 assembly.\"\u003emaster.lib\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTurns out that TH04's player selection menu is exactly three times as\n\tcomplicated as TH05's. Two screens for character and shot type rather than\n\tone, and a way more intricate implementation for saving and restoring the\n\tbackground behind the raised top and left edges of a character picture\n\twhen moving the cursor between Reimu and Marisa. TH04 decides to backup\n\tprecisely only the two 256×8 (top) and 8×244 (left) strips behind the\n\tedges, indicated in \u003cspan style=\"color: red;\"\u003ered\u003c/span\u003e in the picture\n\tbelow.\n\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2020-11-03-TH04-m_char-raise_bg.png?c94ab721\"\u003e\u003cimg src=\"/blog/static/2020-11-03-TH04-m_char-raise_bg.png?c94ab721\" alt=\"Backed-up VRAM area in TH04's player character selection\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003cp\u003e\n\tThese take up just 4 KB of heap memory… but require custom blitting\n\tfunctions, and expanding this explicitly hardcoded approach to TH05's 4\n\tcharacters would have been pretty annoying. So, rather than, uh, \u003ci\u003enot\u003c/i\u003e\n\texplicitly hardcoding it all, ZUN decided to just be lazy with the backup\n\tarea in TH05, saving the entire 640×400 screen, and thus spending 128 KB\n\tof heap memory on this rather simple selection shadow effect.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, this really wasn't something to quickly get done during the first half\n\tof a push, even after already having done TH05's equivalent of this menu.\n\tBut since life is very busy right now, I also used the occasion to start\n\taddressing another code organization annoyance: master.lib's single \u003ccode\u003emaster.h\u003c/code\u003e header file.\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eNow that ReC98 is trying to develop (or at least mimic) a more\n\ttype-safe C++ foundation to model the PC-98 hardware, a pure C header\n\t(with counter-productive C++ extensions) is becoming increasingly\n\tunidiomatic. By moving some of the original assumptions about function\n\tparameters into the type system, we can also reduce the reliance on its\n\tJapanese-only documentation without having to translate it\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eIt's far from complete with regards to providing compile-time PC-98\n\thardware constants and helpful types. In fact,\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6a3246/planar.h\"\u003ewe started\n\tto add these in our own header files\u003c/a\u003e\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/f6a3246/pc98.h\"\u003ea long time\n\tago\u003c/a\u003e.\u003c/li\u003e\n\t\u003cli\u003eIt's quite bloated, with \u003ci\u003eat least\u003c/i\u003e 2800 lines of code that\n\tcurrently are \u003ccode\u003e#include\u003c/code\u003ed into the vast majority of files, not\n\tcounting \u003ccode\u003emaster.h\u003c/code\u003e's recursively included C standard library\n\theaders. PC-98 Touhou only makes direct use of a rather small fraction of\n\tits contents.\u003c/li\u003e\n\t\u003cli\u003eAnd finally, all the DOS/V compatibility definitions are especially\n\tuseless in the context of ReC98. As I've noted\n\t\u003ca href=\"/blog/2020-05-04\"\u003e📝 time\u003c/a\u003e and\n\t\u003ca href=\"/blog/2020-09-17\"\u003e📝 time\u003c/a\u003e again, porting PC-98 Touhou to\n\tIBM-compatible DOS won't be easy, and \u003ccode\u003eMASTER_DOSV\u003c/code\u003e won't be\n\thelping much. Therefore, my upstream version of ReC98 will never include\n\tall of master.lib. There's no point in lengthening compile times for\n\teveryone by default, and those \u003ci\u003ewill\u003c/i\u003e be getting quite noticeable\n\tafter moving to a full 16-bit build process.\u003cbr\u003e\n\t(Actually, what retro system ports should \u003ci\u003erather\u003c/i\u003e be doing: Get rid\n\tof master.lib's original ASM code, replace it with\n\t\u003ca href=\"https://www.youtube.com/watch?v=zBkNBP00wJE\"\u003ereadable, modern\n\tC++, and then simply convert the optimized assembly output of modern\n\tcompilers to your ISA of choice\u003c/a\u003e. Improving the landscape of such\n\tassembly or object file converters would benefit everyone!)\n\u003c/ul\u003e\u003cp\u003e\n\tSo, time to start a new \u003ccode\u003emaster.hpp\u003c/code\u003e header that would contain\n\tjust the declarations from \u003ccode\u003emaster.h\u003c/code\u003e that PC-98 Touhou\n\tactually needs, plus some semantic (yes, semantic) sugar. Comparing just\n\tthe old \u003ccode\u003emaster.h\u003c/code\u003e to just the new \u003ccode\u003emaster.hpp\u003c/code\u003e\n\tafter roughly 60% of the transition has been completed, we get median\n\tbuild times of 319\u0026nbsp;ms for \u003ccode\u003emaster.h\u003c/code\u003e, and 144\u0026nbsp;ms for\n\t\u003ccode\u003emaster.hpp\u003c/code\u003e on my (admittedly rather slow) DOSBox setup.\n\tNice!\u003cbr\u003e\n\tAs of this push, ReC98 consists of 107 translation units that have to be\n\tcompiled with Turbo C++ 4.0J. Fully rebuilding all of these currently\n\ttakes roughly 37.5\u0026nbsp;seconds in DOSBox. After the transition to\n\t\u003ccode\u003emaster.hpp\u003c/code\u003e is done, we could therefore shave some 10 to 15\n\tseconds off this time, simply by switching header files. And that's just\n\tthe beginning, as this will also pave the way for further\n\t\u003ccode\u003e#include\u003c/code\u003e optimizations. Life in this codebase will be great!\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tUnfortunately, there wasn't enough time to repay some of the actual\n\ttechnical debt I was looking forward to, after all of this. Oh well, at\n\tleast we now also have nice identifiers for the three different boldface\n\toptions that are used when rendering text to VRAM, after procrastinating\n\tthat issue for almost 11 months. Next up, assuming the existing\n\tsubscriptions: More ridiculous decompilations of things that definitely\n\t\u003ci\u003eweren't\u003c/i\u003e originally written in C, and a big blocker in TH03's\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-10-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-11-03T01:44:47Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-10-13",
      "url": "https://rec98.nmlgc.net/blog/2020-10-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-10-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-10-13\"\u003e\u003ctime datetime=\"2020-10-13T21:46:07Z\"\u003e2020-10-13 21:46\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0123\"\u003eP0123\u003c/a\u003e\n\t\t\tTH01 decompilation (.BOS format, part 5/5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4406c3d...72dfa09\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cstyle\u003e\n\t#bos-formats-2020-10-13 thead {\n\t\tfont-family: monospace;\n\t}\n\t#bos-formats-2020-10-13 td {\n\t\ttext-align: center;\n\t}\n\u003c/style\u003e\n\n\u003cp\u003e\n\tDone with the .BOS format, at last! While there's still quite a bunch of\n\tundecompiled non-format blitting code left, this was in fact the final\n\tpiece of graphics format loading code in TH01.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2020-09-28\"\u003e📝 Continuing the trend from three pushes ago\u003c/a\u003e,\n\twe've got yet another class, this time for the 48×48 and 48×32 sprites\n\tused in Reimu's gohei, slide, and kick animations. The only reason these\n\thad to use the .BOS format at all is simply because Reimu's regular\n\tsprites are 32×32, and are therefore loaded from\n\t\u003ca href=\"/blog/2020-03-18\"\u003e📝 .PTN files\u003c/a\u003e.\u003cbr\u003e\n\tYes, this makes no sense, because why would you split animations \u003ci\u003efor\n\tthe same character\u003c/i\u003e across two file formats and two APIs, just because\n\tof a sprite size difference?\n\tThis necessity for switching blitting APIs might also explain why Reimu\n\tvanishes for a few frames at the beginning and the end of the gohei swing\n\tanimation, but more on that once we get to the high-level rendering code.\n\u003c/p\u003e\u003cp\u003e\n\tNow that we've decompiled all the .BOS implementations in TH01, here's an\n\toverview of all of them, together with .PTN to show that there really was\n\tno reason for not using the .BOS API for all of Reimu's sprites:\n\u003c/p\u003e\u003ctable id=\"bos-formats-2020-10-13\" class=\"comparison\"\u003e\n\t\u003cthead\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\u003cth\u003eCBossEntity\u003c/th\u003e\n\t\t\t\u003cth\u003eCBossAnim\u003c/th\u003e\n\t\t\t\u003cth\u003eCPlayerAnim\u003c/th\u003e\n\t\t\t\u003cth\u003eptn_* (32×32)\u003c/th\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eFormat\u003c/th\u003e\n\t\t\t\u003ctd\u003e.BOS\u003c/td\u003e\n\t\t\t\u003ctd\u003e.BOS\u003c/td\u003e\n\t\t\t\u003ctd\u003e.BOS\u003c/td\u003e\n\t\t\t\u003ctd\u003e.PTN\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eHitbox\u003c/th\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eByte-aligned blitting\u003c/th\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eByte-aligned unblitting\u003c/th\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003eUnaligned blitting\u003c/th\u003e\n\t\t\t\u003ctd\u003eSingle-line and wave only\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003ePrecise unblitting\u003c/th\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✘\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\t\u003ctd\u003e✔\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003ePer-file sprite limit\u003c/th\u003e\n\t\t\t\u003ctd\u003e8\u003c/td\u003e\n\t\t\t\u003ctd\u003e8\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\t\u003ctd\u003e64\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003cth\u003ePixels blitted at once\u003c/th\u003e\n\t\t\t\u003ctd\u003e16\u003c/td\u003e\n\t\t\t\u003ctd\u003e16\u003c/td\u003e\n\t\t\t\u003ctd\u003e8\u003c/td\u003e\n\t\t\t\u003ctd\u003e32\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\u003cp\u003e\n\tAnd even that last property could simply be handled by branching based on\n\tthe sprite width, and wouldn't be a reason for switching formats. But\n\twell, it just wouldn't be TH01 without all that redundant bloat though,\n\twould it?\n\u003c/p\u003e\u003cp\u003e\n\tThe basic loading, freeing, and blitting code was yet another variation\n\ton the other .BOS code we've seen before. So this should have caused just\n\tas little trouble as the \u003ccode\u003eCBossAnim\u003c/code\u003e code… except that\n\t\u003ccode\u003eCPlayerAnim\u003c/code\u003e \u003ci\u003edid\u003c/i\u003e add one slightly difficult function to\n\tthe mix, which led to it requiring almost a full push after all.\n\tSimilar to \u003ca href=\"/blog/2020-10-06\"\u003e📝 the unblitting code for moving lasers we've seen in the last push\u003c/a\u003e,\n\tZUN tries to minimize the amount of VRAM writes when unblitting Reimu's\n\tslide animations. Technically, it's only necessary to restore the pixels\n\tthat Reimu traveled by, plus the ones that \u003ci\u003ewouldn't\u003c/i\u003e be redrawn by\n\tthe new animation frame at the new X position.\u003cbr\u003e\n\tThe theoretically arbitrary distance between the two sprites is, of\n\tcourse, modeled by a fixed-size buffer on the stack\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e, coming with the further assumption that the\n\tsprite surely hasn't moved by more than 1 horizontal VRAM byte compared to\n\tthe last frame. Which, of course, results in glitches if that's not the\n\tcase, leaving little Reimu parts in VRAM if the slide speed ever exceeded\n\t8 pixels per frame. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e (Which it never does,\n\tbeing hardcoded to 6 pixels, but still.). As it also turns out, all those\n\tbit masking operations easily lead to \u003ci\u003eincredibly\u003c/i\u003e sloppy C code.\n\tWhich compiles into incredibly terrible ASM, which in turn might end up\n\twasting way more CPU time than the final VRAM write optimization would\n\thave gained? Then again, in-depth profiling is way beyond the scope of\n\tthis project at this point.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The TH04 main menu, and some more technical debt.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-11-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-10-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-10-13T21:46:07Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-10-06",
      "url": "https://rec98.nmlgc.net/blog/2020-10-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-10-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-10-06\"\u003e\u003ctime datetime=\"2020-10-06T14:30:34Z\"\u003e2020-10-06 14:30\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0122\"\u003eP0122\u003c/a\u003e\n\t\t\tTH01 decompilation (Shootout lasers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/164591f...4406c3d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/laser\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Beams that can collide with the player. Sometimes difficult.\"\u003elaser\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThis time around, laser is \u003ca href=\"/blog/2018-12-16\"\u003e📝 actually\u003c/a\u003e not\n\tdifficult, with TH01's shootout laser class being simple enough to nicely\n\tfit into a single push. All other stationary lasers (as used by\n\tYuugenMagan, for example) don't even use a class, and are simply treated\n\tas regular lines with collision detection.\n\u003c/p\u003e\u003cp\u003e\n\tBut of course, the shootout lasers also come with the typical share of\n\tTH01 jank we've all come to expect by now. This time, it already starts\n\twith the hardcoded sprite data:\n\u003c/p\u003e\u003cfigure class=\"pixelated\"\u003e\u003cimg\n\tsrc=\"data:image/gif;base64,R0lGODlhgAAIAIAAAAAAAP///yH5BAAAAAAALAAAAACAAAgAAAJtDI6pYerH2ktRTlDty3rxyzEXNlLdFJVQWmatC35oCKqIaLMvPsJx1ZNtaCjbyqgL8n7LIZCZ9AgdpxmyOcNSn9DqyredLowYrzQXdWpNXHX6RtyQ12E0Hf5m579tKXh8dWWGN1i2x3cXF+ZSAAA7\"\n\talt=\"TH01 shootout laser 'sprites'\"\n\tstyle=\"height: 24px;\"\u003e\u003c/figure\n\u003e\u003cp\u003e\n\tA shootout laser can have a width from 1 to 8 pixels, so ZUN stored a\n\tseparate 16×1 sprite with a line for each possible width (left-to-right).\n\tThen, he shifted all of these sprites 1 pixel to the right for all of the\n\t8 possible start positions within a planar VRAM byte (top-to-bottom).\n\tBecause… doing that bit shift programmatically is \u003ci\u003eway\u003c/i\u003e too\n\texpensive, so let's pre-shift at compile time, and use 16× the memory per\n\tsprite? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\u003c/p\u003e\u003cp\u003e\n\tSince a bunch of other sprite sheets need to be pre-shifted as well (this\n\tis the 5th one we've found so far), our sprite converter has a feature to\n\tautomatically generate those pre-shifted variations. This way, we can\n\tabstract away that implementation detail and leave modders with .BMP files\n\tthat still only contain a single version of each sprite. But, uh…, wait,\n\tin this sprite sheet, the second row for 1-pixel lasers is accidentally\n\tshifted right by one more pixel that it should have been?! Which means\n\tthat\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003ewe can't use the auto-preshift feature here, and have to store this\n\tweird-looking (and quite frankly, completely unnecessary) sprite sheet in\n\tits entirety\u003c/li\u003e\n\t\u003cli\u003eZUN did, at least during TH01's development, \u003ci\u003enot\u003c/i\u003e have a sprite\n\tconverter, and directly hardcoded these dot patterns in the C++ code\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\u003c/ol\u003e\u003chr\u003e\u003cp\u003e\n\tThe waste continues with the class itself. 69 bytes, with 22 bytes\n\toutright unused, and 11 not really necessary. As for actual innovations\n\tthough, we've got\n\t\u003ca href=\"/blog/2020-09-12\"\u003e📝 another 32-bit fixed-point type\u003c/a\u003e, this\n\ttime \u003ci\u003eactually\u003c/i\u003e using 8 bits for the fractional part. Therefore, the\n\tray position is tracked to the 1/256th of a pixel, using the full\n\tprecision of master.lib's 8-bit \u003ci\u003esin()\u003c/i\u003e and \u003ci\u003ecos()\u003c/i\u003e lookup\n\ttables.\u003cbr\u003e\n\tUnblitting is also remarkably efficient: It's only done once the laser\n\tstopped extending and started moving, and only for the exact pixels at the\n\tstart of the ray that the laser traveled by in a single frame. If only the\n\tray part was also rendered as efficiently – it's fully blitted every frame,\n\tright next to the collision detection for each row of the ray.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tWith a public interface of two functions (spawn, and update / collide /\n\tunblit / render), that's superficially all there is to lasers in this\n\tgame. There's another (apparently inlined) function though, to both reset\n\tand, uh, \"fully unblit\" all lasers at the end of every boss fight… except\n\tthat it fails hilariously at doing the latter, and ends up effectively\n\tunblitting random 32-pixel line segments, due to ZUN confusing both the\n\tcoordinates and the parameter types for the line unblitting function.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tA while ago, I was asked about\n\t\u003ca href=\"https://youtu.be/Al0KTB_0u4A?t=80\"\u003ethis crash that tends to\n\thappen when defeating Elis\u003c/a\u003e. And while you can clearly see the random\n\tunblitted line segments that are missing from the sprites, I don't\n\t\u003ci\u003equite\u003c/i\u003e think we've found the cause for the crash, since the\n\t\u003ca href=\"/blog/2020-01-14\"\u003e📝 line unblitting function used there\u003c/a\u003e\n\t\u003ci\u003edoes\u003c/i\u003e clip its coordinates to the VRAM range.\n\u003c/p\u003e\u003cp\u003e\n\tNext up: The final piece of image format code in TH01, covering Reimu's\n\tsprites!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-10-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-10-06T14:30:34Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-28",
      "url": "https://rec98.nmlgc.net/blog/2020-09-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-10-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-28\"\u003e\u003ctime datetime=\"2020-09-28T11:52:57Z\"\u003e2020-09-28 11:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0120\"\u003eP0120\u003c/a\u003e\n\t\t\tTH01 decompilation (.BOS format, part 4/5 + Shape blitting)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/453dd3c...3c008b6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0121\"\u003eP0121\u003c/a\u003e\n\t\t\tTH01 decompilation (Invincibility sprites, VRAM effects)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3c008b6...5c42fcd\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBack to TH01, and its boss sprite format… with a separate class for\n\tstoring animations that only differs minutely from the\n\t\u003ca href=\"/blog/2020-08-12\"\u003e📝 regular boss entity class I covered last time\u003c/a\u003e?\n\tDecompiling this class was almost free, and the main reason why the first\n\tof these pushes ended up looking pretty huge.\n\u003c/p\u003e\u003cp\u003e\n\tNext up were the remaining shape drawing functions from the code segment\n\tthat started with the .GRC functions. P0105 already started these with the\n\t(surprisingly sanely implemented) 8×8 diamond, star, and… uh, snowflake\n\t(?) sprites\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAIAQMAAADZb60gAAAABlBMVEUAAH4AAAAuh25gAAAAAXRSTlMAQObYZgAAACZJREFUeAEFwTEBACAAw7DKmJhdyEAAMma9JCShLfOi4mS79JWcfK+gCvOAwD5hAAAAAElFTkSuQmCC\" alt=\"\"\u003e,\n\tprominently seen in the Konngara, Elis, and Sariel fights, respectively.\n\tNow, we've also got:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eellipse arcs with a customizable angle distance between the individual\n\tdots – mostly just used for drawing full circles, though\u003c/li\u003e\n\t\u003cli\u003eline loops – which are only used for the rotating white squares around\n\tMima, meaning that the white star in the YuugenMagan fight got a completely\n\tredundant reimplementation\u003c/li\u003e\n\t\u003cli\u003eand the surprisingly weirdest one, drawing the red invincibility\n\tsprites.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe weirdness becomes obvious with just a single screenshot:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2020-09-28-TH01-Invincibility-sprites.png?86c1ed8e\"\u003e\u003cimg src=\"/blog/static/2020-09-28-TH01-Invincibility-sprites.png?86c1ed8e\" alt=\"TH01 invincibility sprite weirdness\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003cp\u003e\n\tFirst, we've got the obvious issue of the sprites not being clipped at the\n\tright edge of VRAM, with the rightmost pixels in each row of the sprite\n\textending to the beginning of the next row. Well, that's just what you get\n\tif you insist on writing unique low-level blitting code for the majority\n\tof the individual sprites in the game… 🤷\u003cbr\u003e\n\tMore importantly though, the sprite sheet looks like this:\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAIAQMAAACiS2/sAAAABlBMVEUAAKUAAADf0lD4AAAAAXRSTlMAQObYZgAAACBJREFUeF5jYGCQkABhHQYJHZ0oBp26//8hBJgLlgCRAJs0B/0NobvEAAAAAElFTkSuQmCC\" alt=\"\"\u003e\n\tSo how do we even get these fully filled red diamonds?\n\u003c/p\u003e\u003cp\u003e\n\tWell, turns out that the sprites are never consistently unblitted during\n\ttheir 8 frames of animation. There \u003ci\u003eis\u003c/i\u003e a function that \u003ci\u003elooks\u003c/i\u003e\n\tlike it unblits the sprite… except that it starts with by enabling the\n\tGRCG and… \u003ci\u003ereading\u003c/i\u003e from the first bitplane on the background page?\n\tIf this was the EGC, such a read would fill some internal registers with\n\tthe contents of all 4 bitplanes, which can then subsequently be blitted to\n\tall 4 bitplanes of any VRAM page with a single memory write. But with the\n\tGRCG in RMW mode, reads do nothing special, and simply copy the memory\n\tcontents of one bitplane to the read destination. \u003ci\u003eMaybe\u003c/i\u003e ZUN thought\n\tthat setting the RMW color to \u003cspan style=\"color: #ff0000;\"\u003ered\u003c/span\u003e\n\talso sets some internal 4-plane mask register to match that color?\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e \u003cbr\u003e\n\tInstead, the rather random pixels read from the first bitplane are then\n\tused as a mask for a \u003ci\u003esecond\u003c/i\u003e blit of the same red sprite.\n\tEffectively, this only really \"unblits\" the invincibility pixels that are\n\tdrawn on top of Reimu's sprite. Since Reimu is drawn first, the\n\tinvincibility sprites are overwritten anyway. But due to the palette color\n\tlayout of Reimu's sprite, its pixels end up fully masking away any\n\tinvincibility sprite pixels in that second blit, leaving VRAM untouched as\n\ta result. Anywhere else though, this animation quickly turns into the\n\tunion of all animation frames.\n\u003c/p\u003e\u003cp\u003e\n\tThen again, if that 16-dot-aligned rectangular unblitting function is all\n\tyou know about the EGC, and you can't be bothered to write a perfect\n\tunblitter for 8×8 sprites, it becomes obvious why you wouldn't want to use\n\tit:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2020-09-28-TH01-Invincibility-naive-unblitting.webp?5fe6c65c\" preload=\"none\" controls loop width=\"640\" height=\"48\" data-fps=\"56.423132\" data-frame-count=\"244\" style=\"aspect-ratio: 640 / 48\" data-lossless=\"/blog/static/video/zmbv/2020-09-28-TH01-Invincibility-naive-unblitting.avi?8b3a0fe1\"\u003e\u003csource src=\"/blog/static/video/av1/2020-09-28-TH01-Invincibility-naive-unblitting.webm?a63a3fc5\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2020-09-28-TH01-Invincibility-naive-unblitting.webm?560147e1\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2020-09-28-TH01-Invincibility-naive-unblitting.webm?fad11f2d\" type=\"video/webm\"\u003eVideo of naively unblitted TH01 invincibility sprites. \u003ca href=\"/blog/static/video/zmbv/2020-09-28-TH01-Invincibility-naive-unblitting.avi?8b3a0fe1\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tBecause Reimu would barely be visible under all that flicker. In\n\tcomparison, those fully filled diamonds actually look pretty good.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter all that, the remaining time wouldn't have been enough for the next\n\tfew essential classes, so I closed out the push with three more VRAM\n\teffects instead:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eSingle-bitplane pixel inversion inside a 32×32 square – the main effect\n\tbehind the discoloration seen in the bomb animation, as well as the\n\texpanding squares at the end of Kikuri's and Sariel's entrance\n\tanimation\u003c/li\u003e\n\t\u003cli\u003eEGC-accelerated VRAM row copies – the second half of smooth and fully\n\thardware-accelerated scrolling for backgrounds that are twice the size of\n\tVRAM\u003c/li\u003e\n\t\u003cli\u003eAnd finally, the VRAM page content transition function using meshed 8×8\n\tsquares, used for the blocky transition to Sariel's first and second phases.\n\tWhich is quite ridiculous in just how needlessly bloated it is. I'm positive\n\tthat this sort of thing could have also been accelerated using the PC-98's\n\tEGC… although simply writing better C would have already gone a long way.\n\tThe function also comes with three unused mesh patterns.\u003c/li\u003e\n\u003c/ul\u003e\u003chr\u003e\u003cp\u003e\n\tAnd with that, ReC98, as a whole, is not only ⅓ done, but I've also fully\n\tcaught up with the feature backlog for the first time in the history of\n\tthis crowdfunding! Time to go into maintenance mode then, while we wait\n\tfor the next pushes to be funded. Got a huge backlog of tiny maintenance\n\tissues to address at a leisurely pace, and of course there's also the\n\t\u003ca href=\"/blog/2020-09-03\"\u003e📝 16-bit build system\u003c/a\u003e waiting to be\n\tfinished.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-10-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-28T11:52:57Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-21",
      "url": "https://rec98.nmlgc.net/blog/2020-09-21",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-21\"\u003e\u003ctime datetime=\"2020-09-21T13:01:20Z\"\u003e2020-09-21 13:01\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0119\"\u003eP0119\u003c/a\u003e\n\t\t\tTH05 decompilation (Character selection, starting the game)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/cbf14eb...453dd3c\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hidden-content\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Hidden features in the original games.\"\u003ehidden-content\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, TH05 \u003ccode\u003eOP.EXE\u003c/code\u003e. The first half of this push started out\n\tnicely, with an easy decompilation of the entire player character\n\tselection menu. Typical ZUN quality, with not much to say about it. While\n\tthe overall function structure is identical to its TH04 counterpart, the\n\ttwo games only really share small snippets inside these functions, and do\n\tneed to be RE'd separately.\n\u003c/p\u003e\u003cp\u003e\n\tThe high score viewing (not registration) menu would have been next.\n\tUnfortunately, it calls one of the \u003ccode\u003eGENSOU.SCR\u003c/code\u003e loading\n\tfunctions… which are all a complete mess that still needed to be sorted\n\tout first. 5 distinct functions in 6 binaries, and of course TH05 also\n\tmicro-optimized its \u003ccode\u003eMAIN.EXE\u003c/code\u003e version to directly use the DOS\n\t\u003ccode\u003eINT 21h\u003c/code\u003e file loading API instead of master.lib's wrappers.\n\tCould have all been avoided with a single method on the score data\n\tstructure, taking a player character ID and a difficulty level as\n\tparameters…\n\u003c/p\u003e\u003cp\u003e\n\tSo, no score menu in this push then. Looking at the other end of the ASM\n\tcode though, we find the starting functions for the main game, the Extra\n\tStage, and the demo replays, which \u003ci\u003edid\u003c/i\u003e fit perfectly to round out\n\tthis push.\n\u003c/p\u003e\u003cp\u003e\n\tWhich is where we find an easter egg! 🥚 If you've ever looked into\n\t\u003ccode\u003e怪綺談2.DAT\u003c/code\u003e, you might have noticed 6 \u003ccode\u003e.REC\u003c/code\u003e files\n\twith replays for the Demo Play mode. However, the game only ever seems to\n\tcycle between 4 replays. So what's in the other two, and why are they\n\t40\u0026nbsp;KB instead of just 10\u0026nbsp;KB like the others? Turns out that they\n\tcombine into a full Extra Stage Clear replay with Mima, with 3 bombs and 1\n\tdeath, obviously recorded by ZUN himself. The split into two files for the\n\tstage (\u003ccode\u003eDEMO4.REC\u003c/code\u003e) and boss (\u003ccode\u003eDEMO5.REC\u003c/code\u003e) portion is\n\tmerely an attempt to limit the amount of simultaneously allocated heap\n\tmemory.\u003cbr\u003e\n\tTo watch this replay without modding the game, unlock the Extra Stage with\n\tall 4 characters, then hold both the ⬅️ left and ➡️ right arrow keys in the\n\tmain menu while waiting for the usual demo replay.\n\tI can't possibly be the first one to discover this, but I couldn't find\n\tany other mention of it.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2021-03-15):\u003c/strong\u003e ZUN did in fact document this replay\n\tin Section 6 of TH05's \u003ccode\u003eOMAKE.TXT\u003c/code\u003e, along with the exact method\n\tto view it.\n\t\u003ca href=\"https://twitter.com/gensakudan/status/1371411625631223810\"\u003eThanks\n\tto Popfan for the discovery!\u003c/a\u003e\n\u003c/p\u003e\u003cp\u003e\n\tHere's a recording of the whole replay:\n\t\u003cscript\u003e\n\t\texternalRegister('2020-09-21', 'vid', 'https://youtube.com/embed/iP2ywlW2u4U');\n\t\u003c/script\u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ciframe id=\"2020-09-21-vid\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNote how the boss dialogue is skipped. \u003ccode\u003eMAIN.EXE\u003c/code\u003e actually\n\tcontains no less than 6 \u003ccode\u003eif()\u003c/code\u003e branches just to distinguish\n\tthis overly long replay from the regular ones.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tI'd really like to do the TH04 and TH05 main menus in parallel, since we\n\tcan expect a bit more shared code after all the initial differences.\n\tTherefore, I'm going to put the next \"anything\" push towards covering the\n\tTH04 version of those functions. Next up though, it's back to TH01, with\n\tmore redundant image format code…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-21T13:01:20Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-17",
      "url": "https://rec98.nmlgc.net/blog/2020-09-17",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-17\"\u003e\u003ctime datetime=\"2020-09-17T20:58:19Z\"\u003e2020-09-17 20:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0118\"\u003eP0118\u003c/a\u003e\n\t\t\tTH05 PI (100%) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0bb5bc3...cbf14eb\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/unused\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Unused remnants of code. Might tell something about a game\u0026#39;s development history.\"\u003eunused\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t🎉 TH05 is finally fully position-independent! 🎉 To celebrate this\n\tmilestone, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e coded a little demo, which we recorded on\n\tboth an emulator and on real PC-98 hardware:\n\t\u003cscript\u003e\n\t\texternalRegister('2020-09-17', 'hw', 'https://youtube.com/embed/0BmzWRgv27A');\n\t\texternalRegister('2020-09-17', 'emu', 'https://youtube.com/embed/7-UYGhZ1sB8');\n\t\u003c/script\u003e\n\u003c/p\u003e\u003cfigure class=\"side_by_side\"\u003e\n\t\u003ciframe id=\"2020-09-17-hw\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\t\u003ciframe id=\"2020-09-17-emu\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tFor all the new people who are unfamiliar with PC-98 Touhou internals:\n\t\u003ca href=\"/faq#three\"\u003eBoss behavior is hardcoded into\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e, rather than being scriptable via separate .ECL\n\tfiles like in Windows Touhou\u003c/a\u003e. That's what makes this kind of a big\n\tdeal.\n\u003c/p\u003e\u003chr\u003e\u003ch5\u003eWhat does this mean?\u003c/h5\u003e\u003cp\u003e\n\tYou can now freely add or remove both data and code anywhere in TH05, by\n\tediting the ReC98 codebase, writing your mod in ASM or C/C++, and\n\trecompiling the code. Since all absolute memory addresses have now been\n\tconverted to labels, this will work without causing any instability. See\n\tthe \u003ca href=\"/faq#pi-what\"\u003eposition independence section in the FAQ\u003c/a\u003e\n\tfor a more thorough explanation about why this was a problem.\n\u003c/p\u003e\u003cp\u003e\n\tBy extension, this also means that it's now \u003ci\u003etheoretically\u003c/i\u003e possible\n\tto use a different compiler on the source code. \u003cstrong\u003eBut:\u003c/strong\u003e\n\u003c/p\u003e\u003ch5\u003eWhat does this not mean?\u003c/h5\u003e\u003cp\u003e\n\tThe original ZUN code hasn't been completely reverse-engineered yet, let\n\talone decompiled. As the final PC-98 Touhou game, TH05 also happens to\n\thave the largest amount of actual ZUN-written ASM that can't \u003ci\u003eever\u003c/i\u003e\n\tbe decompiled within ReC98's constraints of a legit source code\n\treconstruction. But a lot of the originally-in-C code is also still in\n\tASM, which might make modding a bit inconvenient right now. And while I\n\t\u003ci\u003ehave\u003c/i\u003e decompiled a bunch of functions, I selected them largely\n\tbecause they would help with PI (as requested by the backers), and not\n\tbecause they are particularly relevant to typical modding interests.\n\u003c/p\u003e\u003cp\u003e\n\tAs a result, the code might also be a bit confusingly organized. There's\n\tquite a conflict between various goals there: On the one hand, I'd like to\n\tonly have a single instance of every function shared with earlier games,\n\tas well as reduce ZUN's code duplication within a single game. On the\n\tother hand, this leads to quite a lot of code being scattered all over the\n\tplace and then \u003ccode\u003e#include\u003c/code\u003e-pasted back together, except for the\n\tplaces where\n\t\u003ca href=\"/blog/2020-09-07\"\u003e📝 this doesn't work, and you'd have to use multiple translation units anyway\u003c/a\u003e…\n\tI'm only beginning to figure out the best structure here, and some more\n\treverse-engineering attention surely won't hurt.\n\u003c/p\u003e\u003cp\u003e\n\tAlso, keep in mind that the code still targets x86 Real Mode. To work\n\teffectively in this codebase, you'd need some familiarity with\n\t\u003ca href=\"https://en.wikipedia.org/wiki/X86_memory_segmentation\"\u003ememory\n\tsegmentation\u003c/a\u003e, and how to express it all in code. This tends to make\n\teven regular C++ development about an order of magnitude harder,\n\tespecially once you want to interface with the remaining ASM code. That\n\tpart made \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e struggle quite a bit with implementing his\n\tcustom scripting language for the demo above. For now, he built that demo\n\ton quite a limited foundation – which is why he also chose to release\n\tneither the build nor the source publically for the time being.\u003cbr\u003e\n\tSo yeah, you're definitely going to need the TASM and Borland C++ manuals\n\tthere.\n\u003c/p\u003e\u003cp\u003e\n\ttl;dr: We now know everything about this game's \u003ci\u003edata\u003c/i\u003e, but not quite\n\tas much about this game's \u003ci\u003ecode\u003c/i\u003e.\n\u003c/p\u003e\u003ch5\u003eSo, how long until source ports become a realistic project?\u003c/h5\u003e\u003cp\u003e\n\tYou \u003ci\u003eprobably\u003c/i\u003e want to wait for 100% RE, which is when everything\n\tthat can be decompiled has been decompiled.\n\u003c/p\u003e\u003cp\u003e\n\tUnless your target system is 16-bit Windows, in which case you could\n\ttheoretically start right away. \u003ca href=\"/blog/2020-05-04\"\u003e📝 Again\u003c/a\u003e,\n\tthis would be the ideal first system to port PC-98 Touhou to: It would\n\trequire all the generic portability work to remove the dependency on PC-98\n\t hardware, thus paving the way for a subsequent port to modern systems,\n\t yet you could still just drop in any undecompiled ASM.\n\u003c/p\u003e\u003cp\u003e\n\tPorting to IBM-compatible DOS would only be a harder and less universally\n\tuseful version of that. You'd then simply exchange one architecture, with\n\tits idiosyncrasies and limits, for another, with its own set of\n\tidiosyncrasies and limits. (Unless, of course, you already happen to be\n\tintimately familiar with that architecture.) The fact that master.lib\n\tprovides DOS/V support would have only mattered if ZUN consistently used\n\tit to abstract away PC-98 hardware at every single place in the code,\n\twhich is \u003ci\u003edefinitely\u003c/i\u003e not the case.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe list of actually interesting findings in this push is,\n\t\u003ca href=\"/blog/2019-12-29\"\u003e📝 again\u003c/a\u003e, very short. Probably the most\n\tnotable discovery: The low-level part of the code that renders Marisa's\n\tlaser from her TH04 \u003ci\u003eIllusion Laser\u003c/i\u003e shot type is still present in\n\tTH05. Insert wild mass guessing about potential beta version shot types…\n\tOh, and did you know that the order of background images in the Extra\n\tStage staff roll differs by character?\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Finally driving up the RE% bar again, by decompiling some TH05\n\tmain menu code.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-17T20:58:19Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-16",
      "url": "https://rec98.nmlgc.net/blog/2020-09-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-16\"\u003e\u003ctime datetime=\"2020-09-16T21:00:58Z\"\u003e2020-09-16 21:00\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0117\"\u003eP0117\u003c/a\u003e\n\t\t\tRebuilding ZUN.COM + overall position independence \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/03048c3...0bb5bc3\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gaiji\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pipeline\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Additional, helpful code generation steps, beyond regular compiler/assembler invocations.\"\u003epipeline\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWouldn't it be a bit disappointing to have TH05 completely\n\tposition-independent, but have it still require hex-editing of the\n\toriginal \u003ccode\u003eZUN.COM\u003c/code\u003e to mod its gaiji characters? As in, these\n\tcustom \"text\" glyphs, available to the PC-98 text RAM:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\n\thref=\"/blog/static/2020-09-16-TH05-gaiji.png?4174b6a5\"\u003e\u003cimg src=\"/blog/static/2020-09-16-TH05-gaiji.png?4174b6a5\" alt=\"TH05 gaiji characters\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003cp\u003e\n\tEspecially since we now even have a sprite converter… the lack of which\n\twas exactly \u003ca href=\"/blog/2020-02-23\"\u003e📝 what made rebuilding \u003ccode\u003eZUN.COM\u003c/code\u003e not that worthwhile before\u003c/a\u003e.\n\tSo, before the big release, let's get all the remaining\n\t\u003ccode\u003eZUN.COM\u003c/code\u003e sub-binaries of TH04 and TH05 dumped into .ASM files,\n\tand re-assembled and linked during the build process.\n\u003c/p\u003e\u003cp\u003e\n\tThis is also the moment in which \u003ca class=\"customer\" href=\"https://opensrc.club/\"\u003eEgor\u003c/a\u003e's 2018\n\treimplementation of O.\u0026nbsp;Morikawa's \u003ccode\u003ecomcstm\u003c/code\u003e finally gets\n\tto shine. Back then, I considered it too early to even bother with\n\t\u003ccode\u003eZUN.COM\u003c/code\u003e and reimplementing the .COM wrapper that ZUN\n\toriginally used to bundle multiple smaller executables into that single\n\tbinary. But now that the time is right, it \u003ci\u003eis\u003c/i\u003e nice to have that\n\tcode, as it allowed me to get these rebuilds done in half a push.\n\tOtherwise, it would have surely required one or two dedicated ones.\n\u003c/p\u003e\u003cp\u003e\n\tSince we like correctness here, newly dumped ZUN code means that it also\n\thas to be included in \u003ca href=\"/progress/re-baseline\"\u003ethe RE%\n\tbaseline calculation\u003c/a\u003e. This is why TH04's and TH05's overall RE% bars\n\thave gone back a tiny bit… in case you remember how they previously looked\n\tlike \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e After all, I \u003ci\u003ewould\u003c/i\u003e like to figure\n\tout where all that memory allocated during TH04's and TH05's memory check\n\tis freed, if at all.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, one half of a push left… Y'know, getting rid of those last few PI\n\tfalse positives is actually one of the most annoying chores in this\n\tproject, and quite stressful as well: I have to convince myself that the\n\tremaining false positives are, in fact, not memory references, but with\n\tway too little time for in-depth RE and to denote what they are\n\t\u003ci\u003einstead\u003c/i\u003e. In that situation, \u003ci\u003eeveryone\u003c/i\u003e (including myself!)\n\t\u003ci\u003eis anticipating that PI goal, and no one is really interested in RE\u003c/i\u003e.\n\t(Well… that is, until they actually get to developing their mod. But more\n\ton that tomorrow. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e) Which means that it boils\n\tdown to quite some hasty, dumb, and superficial RE around those remaining\n\tnumbers.\n\u003c/p\u003e\u003cp\u003e\n\tSo, in the hope of making it less annoying for the other 4 games in the\n\tfuture, let's systematically cover the sources of those remaining false\n\tpositives in TH05, over all games. I/O port accesses with either the port\n\tor the value in registers (and thus, no longer as an immediate argument to\n\tthe \u003ccode\u003eIN\u003c/code\u003e or \u003ccode\u003eOUT\u003c/code\u003e instructions, which the PI counter\n\tcan clearly ingore), palette color arithmetic, or heck, 0xFF constants that\n\tobviously just mean \"-1\" and are \u003ci\u003enot\u003c/i\u003e a reference to offset 0xFF in\n\tthe data segment. All of this, of course, once again had a way bigger\n\teffect on everything \u003ci\u003ebut\u003c/i\u003e an almost position-independent TH05… but\n\they, that's the sort of thing you reserve the \"anything\" pushes for. And\n\tthat's also how we get some of the single biggest PI% gains we have seen\n\tso far, and will be seeing before the 100% PI mark. And yes, those will\n\tcontinue in the next push.\n\u003c/p\u003e\u003cp\u003e\n\tAlright! Big release tomorrow…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-16T21:00:58Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-12",
      "url": "https://rec98.nmlgc.net/blog/2020-09-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-12\"\u003e\u003ctime datetime=\"2020-09-12T10:09:54Z\"\u003e2020-09-12 10:09\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0115\"\u003eP0115\u003c/a\u003e\n\t\t\tTH05 RE (Staff roll animation structures)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/967bb8b...e5328a3\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0116\"\u003eP0116\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Floating glyph ball structure + ending data)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e5328a3...03048c3\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e, Blue Bolt, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/cutscene\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Endings or staff roll animations. Also applies to the cutscenes before stages 8 and 9 in TH03.\"\u003ecutscene\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tFinally, after a long while, we've got two pushes with barely anything to\n\ttalk about! Continuing the road towards 100% PI for TH05, these were\n\texactly the two pushes that TH05 \u003ccode\u003eMAINE.EXE\u003c/code\u003e PI was estimated\n\tto additionally cost, relative to TH04's. Consequently, they mostly went\n\tto TH05's unique data structures in the ending cutscenes, the score name\n\tregistration menu, and the\n\t\u003ca href=\"https://youtu.be/QBqwpZzJCNI?t=3\"\u003estaff roll\u003c/a\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tA unique feature in there is TH05's support for automatic text color\n\tchanges in its ending scripts, based on the first full-width Shift-JIS\n\tcodepoint in a line. The \u003ccode\u003e\\c=\u003ci\u003ecodepoint\u003c/i\u003e,\u003ci\u003ecolor\u003c/i\u003e\u003c/code\u003e\n\tcommands at the top of the \u003ccode\u003e_ED??.TXT\u003c/code\u003e set up exactly this\n\tcodepoint→color mapping. As far as I can tell, TH05 is the only Touhou\n\tgame with a feature like this – even the Windows Touhou games went back to\n\tmanually spelling out each color change.\n\u003c/p\u003e\u003cp\u003e\n\tThe orb particles in TH05's staff roll also try to be a bit unique by\n\tusing 32-bit X and Y subpixel variables for their current position. With\n\tstill just 4 fractional bits, I can't really tell yet whether the extended\n\trange was actually necessary. Maybe due to how the \"camera scrolling\"\n\tthrough \"space\" was implemented? All other entities were pretty much the\n\tusual fare, though.\u003cbr\u003e\n\t12.4, 4.4, and now a 28.4 fixed-point format… yup,\n\t\u003ca href=\"/blog/2019-12-05\"\u003e📝 C++ \u003ccode\u003etemplate\u003c/code\u003es\u003c/a\u003e were\n\tdefinitely the right choice.\n\u003c/p\u003e\u003cp\u003e\n\tAt the end of its staff roll, TH05 not only displays\n\t\u003ca href=\"https://youtu.be/QBqwpZzJCNI?t=167\"\u003ethe usual performance\n\tverdict, but then scrolls in the scores at the end of each stage\u003c/a\u003e\n\tbefore switching to the high score menu. The simplest way to smoothly\n\tscroll between two full screens on a PC-98 involves a separate bitmap…\n\twhich is exactly what TH05 does here, reserving 28,160 bytes of its global\n\tdata segment for just one overly large monochrome 320×704 bitmap where\n\tboth the screens are rendered to. That's… one benefit of splitting your\n\tgame into multiple executables, I guess? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\t\u003cbr\u003e\n\tNot sure if it's common knowledge that you can actually scroll back and\n\tforth between the two screens with the Up and Down keys before moving to\n\tthe score menu. I surely didn't know that before. But it makes sense –\n\tmight as well get the most out of that memory.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThe necessary groundwork for all of this \u003ci\u003emay\u003c/i\u003e have actually made\n\tTH04's (yes, TH04's) \u003ccode\u003eMAINE.EXE\u003c/code\u003e technically\n\tposition-independent. Didn't quite reach the same goal for TH05's – but\n\twhat we \u003ci\u003edid\u003c/i\u003e reach is ⅔ of all PC-98 Touhou code now being\n\tposition-independent! Next up: Celebrating even more milestones, as\n\t\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e is about to finish development on his TH05\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e PI demo…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-12T10:09:54Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-07",
      "url": "https://rec98.nmlgc.net/blog/2020-09-07",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-07\"\u003e\u003ctime datetime=\"2020-09-07T19:19:14Z\"\u003e2020-09-07 19:19\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0113\"\u003eP0113\u003c/a\u003e\n\t\t\tTooling (Sprite converter ergonomics)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/150d2c6...6204fdd\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0114\"\u003eP0114\u003c/a\u003e\n\t\t\tSeparating translation units, part 1/10 (focused around TH02 / TH03)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6204fdd...967bb8b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/Lmocinemod\"\u003eLmocinemod\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pipeline\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Additional, helpful code generation steps, beyond regular compiler/assembler invocations.\"\u003epipeline\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/input\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Processing data entered from the keyboard or a joypad.\"\u003einput\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAlright, tooling and technical debt. Shouldn't be really much to talk\n\tabout… oh, wait, this is still ReC98 \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tFor the tooling part, I finished up the remaining ergonomics and error\n\thandling for the\n\t\u003ca href=\"/blog/2020-07-09\"\u003e📝 sprite converter that Jonathan Campbell contributed two months ago\u003c/a\u003e.\n\tWhile I familiarized myself with the tool, I've actually ran into some\n\tunreported errors myself, so this was sort of important to me. Still got\n\tno command-line help in there, but the error messages can now do that job\n\tprobably even better, since we would have had to write them anyway.\n\u003c/p\u003e\u003cp\u003e\n\tSo, what's up with the technical debt then? Well, by now we've accumulated\n\tquite a number of \u003ca href=\"/blog/2020-08-16\"\u003e📝 ASM code slices\u003c/a\u003e that\n\tneed to be either decompiled or clearly marked as undecompilable. Since we\n\tdefine those slices as \"already reverse-engineered\", that decision won't\n\taffect the numbers on the front page at all. But for a complete\n\tdecompilation, we'd still have to do this \u003ci\u003esomeday\u003c/i\u003e. So, rather than\n\tincorporating this work into pushes that were purchased with the\n\texpectation of measurable progress in a certain area, let's take the\n\t\"anything goes\" pushes, and focus entirely on that during them.\n\u003c/p\u003e\u003cp\u003e\n\tThe second code segment seemed like the best place to start with this,\n\tsince it affects the largest number of games simultaneously. Starting with\n\tTH02, this segment contains a set of random \"core\" functions needed by the\n\tbinary. Image formats, sounds, input, math, it's all there in some\n\tcapacity. You could \u003ci\u003emaybe\u003c/i\u003e call it all \"libzun\" or something like\n\tthat? But for the time being, I simply went with the obvious name,\n\t\u003ccode\u003eseg2\u003c/code\u003e. Maybe I'll come up with something more convincing in\n\tthe future.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOh, but wait, why were we assembling all the previous undecompilable ASM\n\ttranslation units in the 16-bit build part? By moving those to the 32-bit\n\tpart, we don't even need a 16-bit TASM in our list of dependencies, as\n\tlong as our build process is not fully 16-bit.\u003cbr\u003e\n\tAnd with that, ReC98 now also builds on Windows 95, and thus, every 32-bit\n\tWindows version. 🎉 Which is certainly the most user-visible improvement\n\tin all of these two pushes. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tBack in 2015, I already decompiled all of TH02's \u003ccode\u003eseg2\u003c/code\u003e\n\tfunctions. As suggested by the Borland compiler, I tried to follow a \"one\n\ttranslation unit per segment\" layout, bundling the binary-specific\n\tcontents via \u003ccode\u003e#include\u003c/code\u003e. In the end, it required two\n\ttranslation units – and that was even \u003ci\u003eafter\u003c/i\u003e manually inserting the\n\toriginal padding bytes via \u003ccode\u003e#pragma codestring\u003c/code\u003e… yuck. But it\n\tworked, compiled, and kept the linker's job (and, by extension,\n\tsegmentation worries) to a minimum. And as long as it all matched the\n\toriginal binaries, it still counted as a valid reconstruction of ZUN's\n\tcode. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tHowever, that idea ultimately falls apart once TH03 starts mixing\n\tundecompilable ASM code inbetween C functions. Now, we officially have no\n\tchoice but to use multiple C and ASM translation units, with maybe only\n\tjust one or two \u003ccode\u003e#include\u003c/code\u003es in them…\n\u003c/p\u003e\u003cp\u003e\n\t…or we finally start reconstructing the actual \u003ccode\u003eseg2\u003c/code\u003e library,\n\tturning every sequence of related functions into its own translation unit.\n\tThis way, we can simply reuse the once-compiled .OBJ files for all the\n\tbinaries those functions appear in, without requiring that additional\n\tlayer of translation units mirroring the original segmentation.\u003cbr\u003e\n\tThe best example for this is\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/ecc1372842ad2872f38ce7f0f134dfff6580ae15/th03/hfliplut.c\"\u003eTH03's\n\talmost undecompilable function that generates a lookup table for\n\thorizontally flipping 8 1bpp pixels\u003c/a\u003e. It's part of every binary since\n\tTH03, but only used in that game. With the previous approach, we would\n\thave had to add 9 C translation units, which would all have just\n\t\u003ccode\u003e#include\u003c/code\u003ed that one file. Now, we simply put the .OBJ file\n\tinto the correct place on the linker command line, as soon as we can.\n\u003c/p\u003e\u003cp\u003e\n\t💡 And suddenly, the linker just inserts the correct padding bytes itself.\n\u003c/p\u003e\u003cp\u003e\n\tThe most immediate gains there also happened to come from TH03. Which is\n\talso where we \u003ci\u003edid\u003c/i\u003e get some tiny RE% and PI% gains out of this after\n\tall, by reverse-engineering some of its sprite blitting setup code. Sure,\n\tI should have done even more RE here, to also cover those 5 functions at\n\tthe end of code segment #2 in TH03's \u003ccode\u003eMAIN.EXE\u003c/code\u003e that were in\n\tfront of a number of library functions I already covered in this push. But\n\tlet's leave that to an actual RE push 😛\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAll in all though, I was just getting started with this; the \u003ci\u003ereal\u003c/i\u003e\n\tgains in terms of removed ASM files are still to come. \u003ci\u003eBut\u003c/i\u003e in the\n\tmeantime, the funding situation has become even better in terms of\n\tallowing me to focus on things nobody asked for. 🙂 So here's a slightly\n\tbetter idea: Instead of spending two more pushes on this, let's shoot for\n\tTH05 \u003ccode\u003eMAINE.EXE\u003c/code\u003e position independence next. If I manage to get\n\tit done, we'll have a 100% position-independent TH05 by the time\n\t\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e finishes his \u003ccode\u003eMAIN.EXE\u003c/code\u003e PI demo, rather\n\tthan the 94% we'd get from just \u003ccode\u003eMAIN.EXE\u003c/code\u003e. That's bound to\n\tmake a much better impression on all the people who will then\n\t(re-)discover the project.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-09-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-07T19:19:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-09-03",
      "url": "https://rec98.nmlgc.net/blog/2020-09-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-09-03\"\u003e\u003ctime datetime=\"2020-09-03T23:26:18Z\"\u003e2020-09-03 23:26\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0001\"\u003eP0001\u003c/a\u003e\n\t\t\tBuild system improvements, part 1 (Using Tup for the 32-bit build)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e447a2d...150d2c6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eGhostPhanom\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t(tl;dr: ReC98 has switched to \u003ca href=\"http://gittup.org/tup\"\u003eTup\u003c/a\u003e for\n\tthe 32-bit build. You probably want to get \u003ca href=\"/blog/static/2020-09-03-tup-bac02a5-win32.zip?4fbd7de0\"\u003e\u003cstrong\u003e\n\t💾\u0026nbsp;this build of Tup\u003c/strong\u003e\u003c/a\u003e, and put it somewhere in your\n\t\u003ccode\u003ePATH\u003c/code\u003e. It's optional, and always will be, but highly\n\trecommended.)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tP0001! Reserved for the delivery of the very first financial contribution\n\tI've ever received for ReC98, back in January 2018. GhostPhanom\n\trequested the exact opposite of immediate results, which motivated me to\n\tgo on quite a passionate quest for the perfect ReC98 build system. A quest\n\tthat went way beyond the crowdfunding…\n\u003c/p\u003e\u003cp\u003e\n\tMakefiles are a decent idea in theory: Specify the targets to generate,\n\tthe source files these targets depend on and are generated from, and the\n\trules to do the generating, with some helpful shorthand syntax. Then, you\n\thave a build dependency graph, and your \u003ccode\u003emake\u003c/code\u003e tool of choice\n\tcan provide minimal rebuilds of only the targets whose sources changed\n\tsince the last \u003ccode\u003emake\u003c/code\u003e call. But, uh… wait, this is C/C++ we're\n\ttalking about, and doesn't pretty much every source file come with a\n\tsecond set of dependent source files, namely, \u003ci\u003eevery single\n\t\u003ccode\u003e#include\u003c/code\u003e in the source file itself\u003c/i\u003e? Do we \u003ci\u003ereally\u003c/i\u003e\n\thave to duplicate all these inside the Makefile, and keep it in sync with the source file? 🙄\n\u003c/p\u003e\u003cp\u003e\n\tThis fact alone means that Makefiles are inherently unsuited for\n\t\u003ci\u003eany\u003c/i\u003e language with an \u003ccode\u003e#include\u003c/code\u003e feature… that is, pretty\n\tmuch every language out there. Not to mention other aspects like changes\n\tto the compilation command lines, or the build rules themselves, all of\n\twhich require metadata of the previous build to be persistently stored in\n\tsome way. I have no idea why such a trash technology is even touted as a\n\tviable build tool for code.\n\u003c/p\u003e\u003cp\u003e\n\tBut wait! Most \u003ccode\u003emake\u003c/code\u003e implementations, including Borland's, do\n\tsupport the notion of \u003ci\u003eauto-dependency\u003c/i\u003e information, emitted by the\n\tcompiler in a specific format, to provide \u003ccode\u003emake\u003c/code\u003e with the\n\tadditional list of \u003ccode\u003e#include\u003c/code\u003es. Sure, this should be a basic\n\tfeature of any self-respecting build tool, and not something you have to\n\t\u003ci\u003eadd\u003c/i\u003e as an \u003ci\u003eextension\u003c/i\u003e, but let's just set our idealism aside\n\tfor a moment. Well, too bad that Borland's implementation\n\t\u003ca href=\"https://github.com/nmlgc/ReCBMake\"\u003eonly works if you spell out\n\tboth the \u003ci\u003esource➜object\u003c/i\u003e and the \u003ci\u003eobject➜binary\u003c/i\u003e rules, which\n\tloses the performance gained from compiling multiple translation units in\n\ta single \u003ccode\u003eBCC\u003c/code\u003e or \u003ccode\u003eTCC\u003c/code\u003e process. And even then, it\n\ttends to break in that DOS VM you're probably using.\u003c/a\u003e Not to mention,\n\t\u003ci\u003eagain\u003c/i\u003e, all the other aspects that still remain unsolved.\n\u003c/p\u003e\u003cp\u003e\n\tSo, I decided to just\n\t\u003ca href=\"https://activitypub.nmlgc.net/@rec98/statuses/01DJE866685JA15FXGR9FWP02M\"\u003e\n\twrite my own build system\u003c/a\u003e, tailor-made for the needs of ReC98's 16-bit\n\tbuild process, and combining a number of experimental ideas. Which is\n\t\u003ci\u003estill\u003c/i\u003e not quite bug-free and ready for public use, given that the\n\tentire past year has kept me busy with actual tangible RE and PI progress.\n\tWhat \u003ci\u003edid\u003c/i\u003e finally become ready, however, is the improvement for the\n\t32-bit build part, and that's what we've got here.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\t💭 Now, if only there was a build system that would perfectly track\n\tdependencies of \u003ci\u003eany\u003c/i\u003e compiler it calls, by injecting code and\n\thooking file opening syscalls. It'd be completely unrealistic for it to\n\talso run on DOS (and we probably don't want to traverse a graph database\n\tin a cycle-limited DOSBox), but it would be perfect for our 32-bit build\n\tpart, as long as that one still exists.\n\u003c/p\u003e\u003cp\u003e\n\tTurns out \u003ca href=\"http://gittup.org/tup/\"\u003eTup\u003c/a\u003e is exactly that system.\n\tIn practice, its low-level nature as a \u003ccode\u003emake\u003c/code\u003e replacement does\n\tlimit its general usefulness, which is why you probably haven't seen it\n\tused in a lot of projects. But for something like ReC98 with its reliance\n\ton outdated compilers that aren't supported by any decent high-level tool,\n\tit's exactly the right tool for the job. Also, it's completely beyond me\n\thow \u003ca href=\"https://ninja-build.org/manual.html#ref_headers\"\u003eNinja, the\n\tmost popular \u003ccode\u003emake\u003c/code\u003e replacement these days, was inspired by\n\tTup, yet went a step back to parsing the specific dependency information\n\tproduced by gcc, Clang, and Visual Studio, and \u003ci\u003eonly\u003c/i\u003e those\u003c/a\u003e…\n\u003c/p\u003e\u003cp\u003e\n\tSure, it might seem \u003ci\u003ereally\u003c/i\u003e minor to worry about not unconditionally\n\trebuilding all 32-bit \u003ccode\u003e.asm\u003c/code\u003e files, which just takes a couple\n\tof seconds anyway. But minimal rebuilds in the 32-bit part also provide\n\tthe foundation for minimal rebuilds in the 16-bit part – and those\n\t\u003ccode\u003eTLINK\u003c/code\u003e invocations \u003ci\u003edo\u003c/i\u003e take quite some time after all.\n\u003c/p\u003e\u003cp\u003e\n\tUsing Tup for ReC98 was an idea that dated back to January 2017. Back\n\tthen, I already opened \u003ca href=\"https://github.com/gittup/tup/pull/308\"\u003e\n\tthe pull request with a fix to allow Tup to work together with 32-bit\n\tTASM\u003c/a\u003e. As much as I love Tup though, the fact that it only worked on\n\t64-bit Windows ≥Vista would have meant that we had to exchange perfect\n\tdependency tracking for the ability to build on 32-bit and older Windows\n\tversions \u003ci\u003eat all\u003c/i\u003e. For a project that relies on DOS compilers, this\n\twould have been exactly the wrong trade-off to make.\n\u003c/p\u003e\u003cp\u003e\n\tWhat's worse though: \u003ccode\u003eTLINK\u003c/code\u003e fails to run on modern 32-bit\n\tWindows with \u003ccode\u003eLoader error (0000) : Unrecognized Error\u003c/code\u003e.\n\tTherefore, the set of systems that Tup runs on, and the set of systems\n\tthat can actually compile ReC98's 16-bit build part natively, would have\n\tbeen exactly disjoint, with no OS getting to use both at the same time.\n\t\u003cbr\u003e\n\tSo I've kept using Tup for only my own development, but indefinitely\n\tshelved the idea of making it the official build system, due to those\n\tdrawbacks. Recently though, it all came together:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe \u003ccode\u003etup generate\u003c/code\u003e sub-command can generate a\n\t\u003ccode\u003e.bat\u003c/code\u003e file that does a full dumb rebuild of everything, which\n\tcan serve as a fallback option for systems that can't run Tup. All we have\n\tto do is to commit that \u003ccode\u003e.bat\u003c/code\u003e file to the ReC98 Git repository\n\tas well, and tell \u003ccode\u003ebuild32b.bat\u003c/code\u003e to fall back on that if Tup\n\tcan't be run. That alone would have given us the benefits of Tup without\n\tbeing worse than the current dumb build process.\u003c/li\u003e\n\t\u003cli\u003eIn the meantime, other contributors improved Tup's own build process to\n\tthe point where 32-bit builds were simple enough to accomplish from the\n\tcomfort of a WSL terminal.\u003c/li\u003e\n\t\u003cli\u003e\u003ca href=\"https://github.com/gittup/tup/pull/406\"\u003eTwo commits of mine\n\tlater\u003c/a\u003e, and 32-bit Windows Tup was fully functional. Another one later,\n\tand 32-bit Windows Tup even gained one potential advantage over its 64-bit\n\tcounterpart. Since it only has to support DLL injection into 32-bit\n\tprograms, it doesn't need a separate 32-bit binary for retrieving function\n\tpointers to the 32-bit version of Windows' DLL loading syscalls. Weirdly\n\tenough, Windows Defender on current Windows 10 falsely flags that binary as\n\tmalware, despite it doing \u003ci\u003enothing but printing those pointer values to\n\tstdout\u003c/i\u003e. 🤷\u003c/li\u003e\n\t\u003cli\u003eAnd that \u003ccode\u003eTLINK\u003c/code\u003e bug?\n\t\u003ca href=\"http://oshow.txt-nifty.com/blog/2008/11/loader-error-00.html\"\u003eEasily\n\tsolved by a Google search\u003c/a\u003e, and by editing\n\t\u003ccode\u003e%WINDIR%\\System32\\autoexec.nt\u003c/code\u003e and rebooting afterwards:\n\t\u003cpre class=\"chroma\"\u003e REM Install DPMI support\n\u003cspan class=\"gd\"\u003e-LH %SystemRoot%\\system32\\dosx\n\u003c/span\u003e\u003cspan class=\"gi\"\u003e+%SystemRoot%\\system32\\dosx\u003c/span\u003e\u003c/pre\u003e\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs I'm writing this post, the pull request has unfortunately not yet been\n\tmerged. So, here's my own custom build instead:\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/static/2020-09-03-tup-bac02a5-win32.zip?4fbd7de0\"\u003e\u003cstrong\u003e💾 Download Tup for 32-bit Windows\u003c/strong\u003e\u003c/a\u003e\n\t\u003csmall\u003e(optimized build at\n\t\u003ca href=\"https://github.com/nmlgc/tup/commit/bac02a5178f6f8edac4ec3bce4d46825785f9f21\"\u003ethis\n\tcommit\u003c/a\u003e)\u003c/small\u003e\n\u003c/p\u003e\u003cp\u003e\n\tI've also added it to the DevKit, for any newcomers to ReC98.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter the switch to Tup and the fallback option, I extensively tested\n\tbuilding ReC98 on all operating systems I had lying around. And holy cow,\n\tso much in that build was broken beyond belief. In the end, the solution\n\tinvolved just fully rebuilding the entire 16-bit part by default.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Which, of course, nullifies any of the\n\tadvantages we might have gotten from a Makefile in the first place, due to\n\tjust how unreliable they are. If you had problems building ReC98 in the\n\tpast, try again now!\n\u003c/p\u003e\u003cp\u003e\n\tAnd sure, it would certainly be possible to also get Tup working on\n\tWindows ≤XP, or 9x even. But I leave that to all those tinkerers out there\n\twho are actually motivated to keep those OSes alive. My work here is\n\tdone\u0026nbsp;–\u0026nbsp;we now have a build process that is optimal on 32-bit\n\tWindows ≧Vista, and still functional \u003ci\u003eand\u003c/i\u003e reliable on 64-bit\n\tWindows, Linux, and everything down to Windows 98 SE, and therefore also\n\treal PC-98 hardware. Pretty good, I'd say.\n\u003c/p\u003e\u003cp\u003e\n\t(If it weren't for that weird crash of the 16-bit \u003ccode\u003eTASM.EXE\u003c/code\u003e in\n\tthat Windows 95 command prompt I've tried it in, it would also work on\n\tthat OS. Probably just a misconfiguration on my part?)\n\u003c/p\u003e\u003cp\u003e\n\tNow, it might look like a waste of time to improve a 32-bit build part\n\tthat won't even exist anymore once this project is done. However, a fully\n\t16-bit DOS build will only make sense after\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003emaster.lib has been turned into a proper library, linked in by\n\t\u003ccode\u003eTLINK\u003c/code\u003e rather than \u003ccode\u003e#include\u003c/code\u003ed in the big .ASM\n\tfiles.\u003c/li\u003e\n\t\u003cli\u003eThis affects all games. If master.lib's data was consistently placed at\n\tthe beginning or end of each data segment, this would be no big deal, but\n\tit's placed somewhere else in every binary.\u003c/li\u003e\n\t\u003cli\u003eSo, this will only make sense sometime around 90% overall PI, and maybe\n\t~50% RE \u003ci\u003ein each game\u003c/i\u003e. Which is something else than 50% overall –\n\t\u003ci\u003eespecially\u003c/i\u003e since it includes TH02, the objectively worst Touhou game,\n\twhich hasn't received \u003ci\u003eany\u003c/i\u003e dedicated funding ever.\u003c/li\u003e\n\t\u003cli\u003eThen, it will probably still require a couple of dedicated pushes to\n\tmove all the remaining data to C land.\u003c/li\u003e\n\t\u003cli\u003eOh, and my 16-bit build system project also needs to be done before,\n\tbecause, again, Makefiles are trash and we shouldn't rely on them even\n\tmore.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd who knows whether this project will get funded for that long. So yeah,\n\tthe 32-bit build part will stay with us for quite some more time, and for\n\tall upcoming PI milestones. And with the current build process, it's\n\tpretty much the most minor among all the minor issues I can think of.\n\tLet's all enjoy the performance of a 32-bit build while we can 🙂\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Paying some technical debt while keeping the RE% and PI% in place.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-09-03T23:26:18Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-08-28",
      "url": "https://rec98.nmlgc.net/blog/2020-08-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-19\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-08-28\"\u003e\u003ctime datetime=\"2020-08-28T13:51:18Z\"\u003e2020-08-28 13:51\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0111\"\u003eP0111\u003c/a\u003e\n\t\t\tTH05 RE (Code around the final MAIN.EXE data references, part 1/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8b5c146...4ef4c9e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0112\"\u003eP0112\u003c/a\u003e\n\t\t\tTH05 RE (Code around the final MAIN.EXE data references, part 2/2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4ef4c9e...e447a2d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/ex-alice\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Extra Stage boss.\"\u003eex-alice\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tOnly one newly ordered push since I've reopened the store? Great, that's\n\tall the justification I needed for the extended maintenance delay that was\n\tpart of these two pushes 😛\n\u003c/p\u003e\u003cp\u003e\n\tHaving to write comments to explain whether coordinates are relative to\n\tthe top-left corner of the screen or the top-left corner of the playfield\n\thas finally become old. So, I introduced\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/e447a2d6879fdc9010848de5594acbaf17cb0c16/pc98.h#L5\"\u003edistinct\n\ttypes for all the coordinate systems we typically encounter\u003c/a\u003e, applying\n\tthem to all code decompiled so far. Note how the planar nature of PC-98\n\tVRAM meant that X and Y coordinates also had to be different from each\n\tother. On the X side, there's mainly the distinction between the\n\t[0;\u0026nbsp;640] screen space and the corresponding [0;\u0026nbsp;80] VRAM byte\n\tspace. On the Y side, we also have the [0;\u0026nbsp;400] screen space, but\n\tthe visible area of VRAM might be limited to [0;\u0026nbsp;200] when running in\n\tthe PC-98's line-doubled 640×200 mode. A VRAM Y coordinate also always\n\timplies an added offset for vertical scrolling.\n\t\u003cbr\u003e\n\tDuring all of the code reconstruction, these types can only have a\n\tdocumenting purpose. Turning them into anything more than just\n\t\u003ccode\u003etypedef\u003c/code\u003es to \u003ccode\u003eint\u003c/code\u003e, in order to define conversion\n\toperators between them, simply won't recompile into identical binaries.\n\tModding and porting projects, however, now have a nice foundation for\n\tdoing just that, and can entirely lift coordinate system transformations\n\tinto the type system, without having to proofread all the meaningless\n\t\u003ccode\u003eint\u003c/code\u003e declarations themselves.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, what was left in terms of memory references? EX-Alice's fire waves\n\twere our final unknown entity that can collide with the player. Decently\n\timplemented, with little to say about them.\n\u003c/p\u003e\u003cp\u003e\n\tThat left the bomb animation structures as the one big remaining PI\n\tblocker. They started out nice and simple in TH04, with a small 6-byte\n\tstar animation structure used for both Reimu and Marisa. TH05, however,\n\tgave each character her own animation… and \u003ci\u003ewhat the hell\u003c/i\u003e is going\n\ton with Reimu's blue stars there? Nope, not going to figure this out on\n\tASM level.\n\u003c/p\u003e\u003cp\u003e\n\tA decompilation first required some more bomb-related variables to be\n\tnamed though. Since this was part of a generic RE push, it made sense to\n\tdo this in all 5 games… which then led to nice PI gains in anything\n\t\u003ci\u003ebut\u003c/i\u003e TH05. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Most notably, we now got the\n\t\u003ci\u003e\"pulling all items to player\"\u003c/i\u003e flag in TH04 and TH05, which is\n\tactually separate from bombing. The obvious cheat mod is left as an\n\texercise to the reader.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tSo, TH05 bomb animations. Just like the\n\t\u003ca href=\"/blog/2020-02-29\"\u003e📝 custom entity types of this game\u003c/a\u003e, all 4\n\tcharacters share the same memory, with the superficially same 10-byte\n\tstructure.\u003cbr\u003e\n\tBut let's just look at the very first field. Seen from a low level, it's a\n\tsimple \u003ccode\u003estruct { int x, y; } pos\u003c/code\u003e, storing the current position\n\tof the character-specific bomb animation entity. But all 4 characters use\n\tthis field differently:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eFor Reimu's blue stars, it's the top-left position of each star, in the\n\t12.4 fixed-point format. But unlike the vast majority of these values in\n\tTH04 and TH05, it's relative to the top-left corner of the\n\t\u003ci\u003escreen\u003c/i\u003e, not the playfield. Much better represented as\n\t\u003ccode\u003estruct { Subpixel screen_x, screen_y; } topleft\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eFor Marisa's lasers, it's the center of each circle, as a regular 12.4\n\tfixed-point coordinate, relative to the top-left corner of the playfield.\n\tMuch better represented as\n\t\u003ccode\u003estruct { Subpixel x, y; } center\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eFor Mima's shrinking circles, it's the center of each circle in regular\n\tpixel coordinates. Much better represented as\n\t\u003ccode\u003estruct { screen_x_t x; screen_y_t y; } center\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eFor Yuuka's spinning heart, it's the top-left corner in regular pixel\n\tcoordinates. Much better represented as\n\t\u003ccode\u003estruct { screen_x_t x; screen_y_t y; } topleft\u003c/code\u003e.\u003cbr\u003e\n\tAnd yes, singular. The game is actually smart enough to only store a single\n\theart, and then create the rest of the circle on the fly. (If it were even\n\tsmarter, it wouldn't even use this structure member, but oh well.)\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tTherefore, I decompiled it as 4 separate structures once again, bundled\n\tinto an \u003ccode\u003eunion\u003c/code\u003e of arrays.\n\u003c/p\u003e\u003cp\u003e\n\tAs for Reimu… yup, that's some pointer arithmetic straight out of\n\t\u003cspan\n\t\tclass=\"hovertext\"\n\t\ttitle=\"(TL note: Jigoku means hell.)\"\n\t\u003eJigoku*\u003c/span\u003e for setting and updating the positions of the falling star\n\ttrails. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e While that certainly required several\n\tcomments to wrap my head around the current array positions, the one \"bug\"\n\tin all this arithmetic luckily has no effect on the game.\u003cbr\u003e\n\tThere \u003ci\u003eis\u003c/i\u003e a small glitch with the growing circles, though. They are\n\tspawned at the end of the loop, with their position taken from the star\n\tpointer… but \u003ci\u003eafter\u003c/i\u003e that pointer has already been incremented. On\n\tthe last loop iteration, this leads to an out-of-bounds structure access,\n\twith the position taken from some unknown EX-Alice data, which is 0 during\n\tmost of the game. If you look at the animation, you can easily spot these\n\tbugged circles, consistently growing from the top-left corner (0,\u0026nbsp;0)\n\tof the playfield:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2020-08-28-TH05-Reimu-Bomb.webp?bd2fdf47\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"10\" data-frame-count=\"46\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2020-08-28-TH05-Reimu-Bomb.avi?fffb3f5a\"\u003e\u003csource src=\"/blog/static/video/av1/2020-08-28-TH05-Reimu-Bomb.webm?d85b318e\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2020-08-28-TH05-Reimu-Bomb.webm?3565009b\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2020-08-28-TH05-Reimu-Bomb.webm?cd9f56a1\" type=\"video/webm\"\u003eVideo of Reimu's bomb animation in TH05, demonstrating the consistent circles growing from the top-left corner of the playfield. \u003ca href=\"/blog/static/video/zmbv/2020-08-28-TH05-Reimu-Bomb.avi?fffb3f5a\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003chr\u003e\u003cp\u003e\n\tAfter all that, there was barely enough remaining time to filter out and\n\tlabel the final few memory references. But now, TH05's\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e is \u003ci\u003etechnically\u003c/i\u003e position-independent! 🎉\n\t\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e is going to work on a pretty extensive demo of this\n\tunprecedented level of efficient Touhou game modding. For a more impactful\n\teffect of both the 100% PI mark and that demo, I'll be delaying the push\n\tcovering the remaining false positives in that binary until that demo is\n\tdone. I've accumulated a pretty huge backlog of minor maintenance issues\n\tby now…\u003cbr\u003e\n\tNext up though: The first part of the long-awaited build system\n\timprovements. I've finally come up with a way of sanely accelerating the\n\t32-bit build part on most setups you could possibly want to build ReC98\n\ton, without making the building experience worse for the other few setups.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-09-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-19\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-08-28T13:51:18Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-08-19",
      "url": "https://rec98.nmlgc.net/blog/2020-08-19",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-08-19\"\u003e\u003ctime datetime=\"2020-08-19T18:14:28Z\"\u003e2020-08-19 18:14\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0110\"\u003eP0110\u003c/a\u003e\n\t\t\tTH05 RE (Shinki and EX-Alice background animation structures)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/2c7d86b...8b5c146\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/ex-alice\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Extra Stage boss.\"\u003eex-alice\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t… and just as I explained \u003ca href=\"/blog/2020-08-16\"\u003e📝 in the last post\u003c/a\u003e\n\thow decompilation is typically more sensible and efficient than ASM-level\n\treverse-engineering, we have this push demonstrating a counter-example.\n\tThe reason why the background particles and lines in the Shinki and\n\tEX-Alice battles contributed so much to position dependence was simply\n\tbecause they're accessed in a relatively large amount of functions, one\n\tfor each different animation. Too many to spend the remaining precious\n\tcrowdfunded time on reverse-engineering or even decompiling them all,\n\t\u003ci\u003eespecially\u003c/i\u003e now that everyone anticipates 100% PI for TH05's\n\t\u003ccode\u003eMAIN.EXE\u003c/code\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tTherefore, I only decompiled the two functions of the line structure that\n\talso demonstrate best how it works, which in turn also helped with RE.\n\tSadly, this revealed that we actually \u003ci\u003ecan't\u003c/i\u003e\n\t\u003ca href=\"/blog/2019-09-15\"\u003e📝 overload \u003ccode\u003eoperator =()\u003c/code\u003e\u003c/a\u003e to get\n\tthat nice assignment syntax for 12.4 fixed-point values, because one of\n\tthose new functions relies on Turbo C++'s built-in optimizations for\n\ttrivially copyable structures. Still, impressive that this abstraction\n\tcaused no other issues for almost one year.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the structures themselves… nope, nothing to criticize this time!\n\tSure, one good particle system would have been awesome, instead of having\n\tseparate structures for the Stage 2 \"starfield\" particles and the one used\n\tin Shinki's battle, with hardcoded animations for both. But given the\n\tgame's short development time, that was quite an acceptable compromise,\n\tI'd say.\u003cbr\u003e\n\tAnd as for the lines, there just \u003ci\u003ehas\u003c/i\u003e to be a reason why the game\n\treserves 20 lines per set, but only renders lines #0, #6, #12, and #18.\n\tWe'll probably see once we get to look at those animation functions more\n\tclosely.\n\u003c/p\u003e\u003cp\u003e\n\tThis was quite a \u003ca href=\"/blog/2019-11-29\"\u003e📝 TH03-style\u003c/a\u003e RE push,\n\twhich yielded way more PI% than RE%. But now that that's done, I can\n\tfinally \u003ci\u003enot\u003c/i\u003e get distracted by all that stuff when looking at the\n\tlist of remaining memory references. Next up: The last few missing\n\tstructures in TH05's \u003ccode\u003eMAIN.EXE\u003c/code\u003e!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-08-19T18:14:28Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-08-16",
      "url": "https://rec98.nmlgc.net/blog/2020-08-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-19\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-08-16\"\u003e\u003ctime datetime=\"2020-08-16T19:48:44Z\"\u003e2020-08-16 19:48\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0109\"\u003eP0109\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Boss movement / Bullet group tuning)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dcf4e2c...2c7d86b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBack to TH05! Thanks to the good funding situation, I can strike a nice\n\tbalance between getting TH05 position-independent as quickly as possible,\n\tand properly reverse-engineering some missing important parts of the game.\n\tOnce 100% PI will get the attention of modders, the code will then be in\n\tbetter shape, and a bit more usable than if I just rushed that goal.\n\u003c/p\u003e\u003cp\u003e\n\tBy now, I'm apparently also pretty spoiled by TH01's immediate\n\tdecompilability, after having worked on that game for so long.\n\tReverse-engineering in ASM land \u003ci\u003eis\u003c/i\u003e pretty annoying, after all,\n\tsince it basically boils down to meticulously editing a piece of ASM into\n\tsomething I can confidently call \u003ci\u003e\"reverse-engineered\"\u003c/i\u003e. Most of the\n\ttime, simply decompiling that piece of code would take just a little bit\n\tlonger, but be massively more useful. So, I immediately tried decompiling\n\twith TH05… and it just worked, at every place I tried!? Whatever the issue\n\twas that made \u003ca href=\"/blog/2019-09-21\"\u003e📝 segment splitting\u003c/a\u003e so\n\tannoying at my first attempt, I seem to have completely solved it in the\n\tmeantime. 🤷 So yeah, backers can now request pretty much any part of TH04\n\tand TH05 to be decompiled immediately, with no additional segment\n\tsplitting cost.\n\u003c/p\u003e\u003cp\u003e\n\t(Protip for everyone interested in starting their own ReC project: Just\n\tdeclare one segment per function, right from the start, then group them\n\ttogether to restore the original code segmentation…)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tExcept that TH05 then just throws more of its infamous micro-optimized and\n\tundecompilable ASM at you. 🙄 This push covered the function that adjusts\n\tthe bullet group template based on rank and the selected difficulty,\n\tcalled every time such a group is configured. Which, just like pretty\n\tmuch all of TH05's bullet spawning code, is one of those undecompilable\n\tfunctions. If C allowed labels of other functions as \u003ccode\u003egoto\u003c/code\u003e\n\ttargets, it \u003ci\u003emight\u003c/i\u003e have been decompilable into something useful to\n\tmodders… maybe. But like this, there's no point in even trying.\n\u003c/p\u003e\u003cp\u003e\n\tThis is such a terrible idea from a software architecture point of view, I\n\tcan't even. Because now, you suddenly \u003ci\u003ehave\u003c/i\u003e to mirror your C++\n\tdeclarations in ASM land, and keep them in sync with each other. I'm\n\talways happy when I get to delete an ASM declaration from the codebase\n\tonce I've decompiled all the instances where it was referenced. But for\n\tTH05, we now have to keep those declarations around forever. 😕 And all\n\tthat for a performance increase you probably couldn't even measure. Oh\n\twell, pulling off Galaxy Brain-level ASM optimizations \u003ci\u003eis\u003c/i\u003e kind of\n\tfun if you don't have portability plans… I guess?\n\u003c/p\u003e\u003cp\u003e\n\tIf I started a full fangame mod of a PC-98 Touhou game, I'd base it on\n\tTH04 rather than TH05, and backport selected features from TH05 as\n\tneeded. Just because it was released later doesn't make it better, and\n\tthis is by far not the only one of ZUN's micro-optimizations that just\n\twent way too far.\n\u003c/p\u003e\u003cp\u003e\n\tDropping down to ASM also makes it easier to introduce weird quirks.\n\tDecompiled, one of TH05's tuning conditions for\n\t\u003ca href=\"https://sparen.github.io/ph3tutorials/ddsga3.html#sub5\"\u003estack\n\tgroups\u003c/a\u003e on Easy Mode would look something like:\n\u003c/p\u003e\u003cpre\u003ecase BP_STACK:\n\t// […]\n\tif(spread_angle_delta \u003e= 2) {\n\t\tstack_bullet_count--;\n\t}\u003c/pre\u003e\u003cp\u003e\n\tThe fields of the bullet group template aren't typically reset when\n\tsetting up a new group. So, \u003ccode\u003espread_angle_delta\u003c/code\u003e in the context\n\tof a \u003ci\u003estack\u003c/i\u003e group effectively refers to \"the delta angle of the last\n\t\u003ci\u003espread\u003c/i\u003e group that was fired before this stack – whenever that was\".\n\tuth05win also spotted this quirk, considered it a bug, and wrote\n\tfanfiction by changing \u003ccode\u003espread_angle_delta\u003c/code\u003e to\n\t\u003ccode\u003estack_bullet_count\u003c/code\u003e.\u003cbr\u003e\n\tAs usual for functions that occur in more than one game, I also decompiled\n\tthe TH04 bullet group tuning function, and it's perfectly sane, with no\n\tsuch quirks.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tIn the more PI-focused parts of this push, we got the TH05-exclusive\n\tsmooth boss movement functions, for flying randomly or towards a given\n\tpoint. Pretty unspectacular for the most part, but we've got yet another\n\tuth05win inconsistency in the latter one. Once the Y coordinate gets close\n\tenough to the target point, it actually speeds up twice as much as the\n\tX coordinate would, whereas uth05win used the same speedup factors for\n\tboth. This might make uth05win a couple of frames slower in all boss\n\tfights from Stage 3 on. Hard to measure though – and boss movement partly\n\tdepends on RNG anyway.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tNext up: Shinki's background animations – which are actually the single\n\tbiggest source of position dependence left in TH05.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-19\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-08-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-08-16T19:48:44Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-08-12",
      "url": "https://rec98.nmlgc.net/blog/2020-08-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-08-12\"\u003e\u003ctime datetime=\"2020-08-12T16:09:35Z\"\u003e2020-08-12 16:09\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0105\"\u003eP0105\u003c/a\u003e\n\t\t\tTH01 decompilation (.GRC format / Hardcoded sprites, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/3622eb6...11b776b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0106\"\u003eP0106\u003c/a\u003e\n\t\t\tTH01 decompilation (Boss entity classes / .BOS format, part 1/5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/11b776b...1f1829d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0107\"\u003eP0107\u003c/a\u003e\n\t\t\tTH01 decompilation (Boss entity classes / .BOS format, part 2/5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1f1829d...1650241\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0108\"\u003eP0108\u003c/a\u003e\n\t\t\tTH01 decompilation (Boss entity classes / .BOS format, part 3/5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1650241...dcf4e2c\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/singyoku\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 5 boss.\"\u003esingyoku\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuugenmagan\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 魔界/Makai route.\"\u003eyuugenmagan\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/elis\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 魔界/Makai route.\"\u003eelis\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kikuri\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 地獄/Jigoku route.\"\u003ekikuri\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAnd indeed, I got to end my vacation with \u003ci\u003ea lot\u003c/i\u003e of image format and\n\tblitting code, covering the final two formats, .GRC and .BOS. .GRC was\n\tnothing noteworthy – one function for loading, one function for\n\tbyte-aligned blitting, and one function for freeing memory. That's it –\n\tnot even a unblitting function for this one. .BOS, on the other hand…\n\u003c/p\u003e\u003cp\u003e\n\t…has no generic (read: single/sane) implementation, and is only\n\timplemented as methods of some boss entity class. And then again for\n\tSariel's dress and wand animations, and then \u003ci\u003eagain\u003c/i\u003e for Reimu's\n\tanimations, both of which weren't even part of these 4 pushes. Looking\n\tforward to decompiling essentially the same algorithms all over again… And\n\tthat's how TH01 became the largest and most bloated PC-98 Touhou game. So\n\tyeah, still not done with image formats, even at 44% RE.\n\u003c/p\u003e\u003cp\u003e\n\tThis means I also had to reverse-engineer that \"boss entity\" class… yeah,\n\twhat else to call something a boss can have multiple of, that may or may\n\tnot be part of a larger boss sprite, may or may not be animated, and that\n\tmay or may not have an orb hitbox?\u003cbr\u003e\n\tAll bosses except for Kikuri share the same 5 global instances of this\n\tclass. Since renaming all these variables in ASM land is tedious anyway, I\n\twent the extra mile and directly defined separate, meaningful names for\n\tthe entities of all bosses. These also now document the natural order in\n\twhich the bosses will ultimately be decompiled. So, unless a backer\n\trequests anything else, this order will be:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eKonngara\u003c/li\u003e\n\t\u003cli\u003eSariel\u003c/li\u003e\n\t\u003cli\u003eElis\u003c/li\u003e\n\t\u003cli\u003eKikuri\u003c/li\u003e\n\t\u003cli\u003eSinGyoku\u003c/li\u003e\n\t\u003cli\u003e(code for regular card-flipping stages)\u003c/li\u003e\n\t\u003cli\u003eMima\u003c/li\u003e\n\t\u003cli\u003eYuugenMagan\u003c/li\u003e\n\u003c/ol\u003e\u003chr\u003e\u003cp\u003e\n\tAs everyone kind of expects from TH01 by now, this class reveals yet\n\tanother… um, \u003ci\u003eunique and quirky\u003c/i\u003e piece of code architecture. In\n\taddition to the position and hitbox members you'd expect from a class like\n\tthis, the game also stores the .BOS metadata – width, height, animation\n\tframe count, and \u003ca href=\"/blog/2020-05-31\"\u003e📝 bitplane pointer slot\u003c/a\u003e\n\tnumber – inside the same class. But if each of those still corresponds to\n\tone individual on-screen sprite, how can YuugenMagan have 5 eye sprites,\n\tor Kikuri have more than one soul and tear sprite? By duplicating that\n\tmetadata, of course! And copying it from one entity to another\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\u003cbr\u003e\n\tAt this point, I feel like I even have to congratulate the game for not\n\tactually loading YuugenMagan's eye sprites 5 times. But then again, 53,760\n\tbytes of waste would have definitely been noticeable in the DOS days.\n\tMakes much more sense to waste that amount of space on an unused C++\n\texception handler, and a bunch of redundant, unoptimized blitting\n\tfunctions \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\t(Thinking about it, YuugenMagan fits this entire system perfectly. And\n\ttogether with its position in the game's code – last to be decompiled\n\tmeans first on the linker command line – we might speculate that\n\tYuugenMagan was the first boss to be programmed for TH01?)\n\u003c/p\u003e\u003cp\u003e\n\tSo if a boss wants to use sprites with different sizes, there's no way\n\taround using another entity. And that's why Girl-Elis and Bat-Elis are two\n\tdistinct entities internally, and have to manually sync their position.\n\tExcept that there's also a \u003ci\u003ethird\u003c/i\u003e one for Attacking-Girl-Elis,\n\tbecause Girl-Elis has 9 frames of animation in total, and the global .BOS\n\tbitplane pointers are divided into 4 slots of only \u003ci\u003e8\u003c/i\u003e images each.\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\u003cbr\u003e\n\tSame for SinGyoku, who is split into a \u003ci\u003esphere\u003c/i\u003e entity, a \u003ci\u003e\n\tperson\u003c/i\u003e entity, and a… \u003ci\u003ewhite flash\u003c/i\u003e entity for all three forms,\n\tall at the same resolution. Or Konngara's facial expressions, which also\n\trequire two entities just for themselves.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd once you decompile all this code, you notice just how much of it the\n\tgame didn't even use. 13 of the 50 bytes of the boss entity class are\n\toutright unused, and 10 bytes are used for a movement clamping and lock\n\tsystem that \u003ci\u003ewould\u003c/i\u003e have been nice if ZUN also used it outside of\n\tKikuri's soul sprites. Instead, all other bosses ignore this system\n\tcompletely, and just\n\t\u003ca href=\"https://devblogs.microsoft.com/oldnewthing/20140211-00/?p=1803\"\u003eparty on\u003c/a\u003e\n\tthe X/Y coordinates of the boss entities directly.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the rendering functions, 5 out of 10 are unused. And while those\n\tdefinitely make up \u003ci\u003eless\u003c/i\u003e than half of the code, I still must have\n\tspent at least 1 of those 4 pushes on effectively unused functionality.\n\t\u003cbr\u003e\n\tOnly one of these functions lends itself to some speculation. For Elis'\n\tentrance animation, the class provides functions for wavy blitting and\n\tunblitting, which use a separate X coordinate for every line of the\n\tsprite. But there's also an unused and sort of broken one for unblitting\n\ttwo overlapping wavy sprites, located at the same Y coordinate. This might\n\tindicate that Elis could originally split herself into two sprites,\n\tsimilar to TH04 Stage 6 Yuuka? Or it might just have been some other kind\n\tof animation effect, who knows.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAfter over 3 months of TH01 progress though, it's finally time to look at\n\tother games, to cover the rest of the crowdfunding backlog. Next up: Going\n\tback to TH05, and getting rid of those last PI false positives. And since\n\tI can potentially spend the next 7 weeks on almost full-time ReC98 work,\n\tI've also re-opened the store until October!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-27\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-08-12T16:09:35Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-07-27",
      "url": "https://rec98.nmlgc.net/blog/2020-07-27",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-07-27\"\u003e\u003ctime datetime=\"2020-07-27T15:27:03Z\"\u003e2020-07-27 15:27\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0103\"\u003eP0103\u003c/a\u003e\n\t\t\tTH01 decompilation (HUD, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b60f38d...05c0028\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0104\"\u003eP0104\u003c/a\u003e\n\t\t\tTH01 decompilation (HUD, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/05c0028...3622eb6\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tIt's vacation time! Which, for ReC98, means \"relaxing by looking at\n\tsomething boring and uninteresting that we'll ultimately have to cover\n\tanyway\"… like the TH01 HUD.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca href=\"/blog/2020-03-18\"\u003e📝 As noted earlier\u003c/a\u003e, all the score, card\n\tcombo, stage, and time numbers are drawn into VRAM. Which turns TH01's HUD\n\trendering from the trivial, gaiji-assisted text RAM writes we see in later\n\tgames to something that, once again, requires blitting and unblitting\n\tsteps. For some reason though, everything on there is blitted to \u003ci\u003eboth\n\t\u003c/i\u003e VRAM pages? And that's why the HUD chose to allocate a bunch of .PTN\n\tsprite slots to store the background behind all \"animated\" elements at the\n\tbeginning of a 4-stage scene or boss battle… \u003ci\u003eseparately for every\n\taffected 16×16 area\u003c/i\u003e. (Looking forward to the completely unnecessary\n\tcode in the Sariel fight that updates these slots after the backgrounds\n\twere animated!) And without any separation into helper functions, we end\n\tup with the same blitting calls separately copy-pasted for every single\n\tHUD element. That's why something as seemingly trivial as this isn't even\n\t\u003ci\u003edone\u003c/i\u003e after 2 pushes, as we're still missing the stage timer.\n\u003c/p\u003e\u003cp\u003e\n\tThankfully, the .PTN function signatures come with none of ZUN's little\n\tinconsistencies, so I was able to mostly reduce this copy-pasta to a bunch\n\tof small inline functions and macros. Those interfaces still remain a bit\n\tannoying, though. As a 32×32 format, .PTN merely supports 16×16 sprites\n\twith a separate bunch of functions that take an additional\n\t\u003ccode\u003equarter\u003c/code\u003e parameter from 0 to 3, to select one of the 4 16×16\n\tquarters in a such a sprite…\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tFor life and bomb counts, there was no way around VRAM though, since ZUN\n\twanted to use more than a single color for those. This is where we find at\n\tleast somewhat of a mildly interesting quirk in all of this: Any life\n\tcounts greater than the intended 6 will wrap into new rows, with the bombs\n\tin the second row overlapping those excess lives. With the way the rest of\n\tthe HUD rendering works, that wrapping code code had to be explicitly\n\twritten… which means that ZUN did in fact accomodate (his own?) cheating\n\tthere.\n\u003c/p\u003e\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2020-07-27-Life-wrapping.png?fdb96502\"\u003e\u003cimg src=\"/blog/static/2020-07-27-Life-wrapping.png?fdb96502\" alt=\"TH01 life wrapping\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003chr\u003e\u003cp\u003e\n\tNow, I promised image formats, and in the middle of this copy-pasta, we\n\t\u003ci\u003edid\u003c/i\u003e get one… sort of. \u003ccode\u003eMASK.GRF\u003c/code\u003e, the red HUD\n\tbackground, is entirely handled with two small bespoke functions… and\n\tthat's all the code we have for this format. Basically, it's a variation\n\ton the \u003ca href=\"/blog/2020-03-07\"\u003e📝 .GRZ format we've seen earlier\u003c/a\u003e. It\n\tuses the exact same RLE algorithm, but only has a single byte stream for\n\tboth RLE commands and pixel data… as you would expect from an RLE format.\n\u003c/p\u003e\u003cp\u003e\n\t.GRF actually stores 4 separately encoded RLE streams, which suggests that\n\tit was intended for full 16-color images. Unfortunately,\n\t\u003ccode\u003eMASK.GRF\u003c/code\u003e only contains 4 copies of the same HUD background\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e, so no unused beta data for us there. The only\n\tthing we \u003ci\u003ecould\u003c/i\u003e derive from 4 identical bitplanes would be that the\n\tbackground was originally meant to be drawn using color #15, rather than\n\tthe \u003cspan style=\"color: #ff0000;\"\u003ered\u003c/span\u003e seen in the final game. Color\n\t#15 is a stage-specific background color that \u003ci\u003ewould\u003c/i\u003e have made the\n\tHUD blend in quite nicely – in the YuugenMagan fight, it's the changing\n\tcolor of the \u003cspan lang=\"ja\"\u003e邪\u003c/span\u003e in the background, for example. But\n\treally, with no generic implementation of this format, that's all just\n\tspeculation.\n\u003c/p\u003e\u003cp\u003e\n\tOh, and in case you were looking for a rip of that image:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2020-07-27-mask.grf.png?f5226460\"\u003e\u003cimg src=\"/blog/static/2020-07-27-mask.grf.png?f5226460\" alt=\"TH01 HUD background (MASK.GRF)\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003chr\u003e\u003cp\u003e\n\tSo yeah, more of the usual TH01 code, with the usual small quirks, but\n\tnothing all too horrible – as expected. Next up: The image formats that\n\tdidn't make it into this push.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-08-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-07-27T15:27:03Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-07-12",
      "url": "https://rec98.nmlgc.net/blog/2020-07-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-07-12\"\u003e\u003ctime datetime=\"2020-07-12T14:51:14Z\"\u003e2020-07-12 14:51\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0099\"\u003eP0099\u003c/a\u003e\n\t\t\tTH01 decompilation (Pellets, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1799d67...1b25830\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0100\"\u003eP0100\u003c/a\u003e\n\t\t\tTH01 decompilation (Pellets, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1b25830...ceb81db\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0101\"\u003eP0101\u003c/a\u003e\n\t\t\tTH01 decompilation (Pellets, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ceb81db...c11a956\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0102\"\u003eP0102\u003c/a\u003e\n\t\t\tTH01 decompilation (Pellets, part 4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c11a956...b60f38d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/elis\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 魔界/Makai route.\"\u003eelis\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/kikuri\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 15 boss, on the 地獄/Jigoku route.\"\u003ekikuri\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/sariel\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 魔界/Makai route.\"\u003esariel\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWell, make that three days. Trying to figure out all the details behind\n\tthe sprite flickering was absolutely dreadful…\u003cbr\u003e\n\tIt started out easy enough, though. Unsurprisingly, TH01 had a quite\n\tlimited pellet system compared to TH04 and TH05:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe cap is 100, rather than 240 in TH04 or 180 in TH05.\u003c/li\u003e\n\t\u003cli\u003eOnly 6 special motion functions (with one of them broken and unused)\n\tinstead of 10. This is where you find the code that generates SinGyoku's\n\tchase pellets, Kikuri's small spinning multi-pellet circles, and\n\tKonngara's rain pellets that bounce down from the top of the playfield.\n\t\u003c/li\u003e\n\t\u003cli\u003eA tiny selection of preconfigured multi-pellet groups. Rather than\n\tTH04's and TH05's freely configurable n-way spreads, stacks, and rings,\n\tTH01 only provides abstractions for 2-, 3-, 4-, and 5- way spreads (yup,\n\tno 6-way or beyond), with a fixed narrow or wide angle between the\n\tindividual pellets. The resulting pellets are also hardcoded to linear\n\tmotion, and can't use the special motion functions. Maybe not the best\n\tcode, but still kind of cute, since the generated groups do follow a\n\tclear logic.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAs expected from TH01, the code comes with its fair share of smaller,\n\tinsignificant ZUN bugs and oversights. As you would \u003ci\u003ealso\u003c/i\u003e expect\n\tthough, the sprite flickering points to the biggest and most consequential\n\tflaw in all of this.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tApparently, it started with ZUN getting the impression that it's only\n\tpossible to use the PC-98 EGC for fast blitting of all 4 bitplanes in one\n\tCPU instruction if you blit 16 horizontal pixels (= 2 bytes) at a time.\n\tConsequently, he only wrote one function for EGC-accelerated sprite\n\tunblitting, which can only operate on a \"grid\" of 16×1 tiles in VRAM. But\n\twait, pellets are not only just 8×8, but can also be placed at any\n\tunaligned X position…\n\u003c/p\u003e\u003cp\u003e\n\t… yet the game still insists on using this 16-dot-aligned function to\n\tunblit pellets, forcing itself into using a super sloppy 16×8 rectangle\n\tfor the job. 🤦 ZUN then tried to mitigate the resulting flickering in two\n\thilarious ways that just make it worse:\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eAn… \"interlaced rendering\" mode? This one's activated for all Stage 15\n\tand 20 fights, and separates pellets into two halves that are rendered on\n\talternating frames. Collision detection with the Yin-Yang Orb and the\n\tplayer is only done for the visible half, but collision detection with\n\tplayer \u003ci\u003eshots\u003c/i\u003e is still done for all pellets every frame, as are\n\tmotion updates – so that pellets don't end up moving half as fast as they\n\tshould.\u003cbr\u003e\n\tSo yeah, your eyes weren't deceiving you. The game \u003ci\u003edoes\u003c/i\u003e effectively\n\tdrop its perceived frame rate in the Elis, Kikuri, Sariel, and Konngara\n\tfights, and it does so deliberately.\u003c/li\u003e\n\t\u003cli\u003e\u003cp\u003e\n\t\t\u003ca href=\"/blog/2020-06-13\"\u003e📝 Just like player shots\u003c/a\u003e, pellets\n\t\tare \u003ci\u003ealso\u003c/i\u003e unblitted, moved, and rendered in a single function.\n\t\tThanks to the 16×8 rectangle, there's now the (completely unnecessary)\n\t\tpossibility of accidentally unblitting parts of a sprite that was\n\t\tpreviously drawn into the 8 pixels right of a pellet. And \u003ci\u003ethis\u003c/i\u003e\n\t\tis where ZUN went full \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e and went \"oh, I\n\t\tknow, let's test the entire 16 pixels, and in case we got an entity\n\t\tthere, we simply make the \u003ci\u003epellet\u003c/i\u003e invisible for this frame! Then\n\t\twe don't even have to unblit it later!\" \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tExcept that this is only done for the first 3 elements of the player\n\t\tshot array…?! Which don't even necessarily have to contain the 3 shots\n\t\tfired last. It's not done for the player sprite, the Orb, or, heck,\n\t\t\u003ci\u003eother pellets\u003c/i\u003e that come earlier in the pellet array. (At least\n\t\twe avoided going 𝑂(𝑛²) there?)\n\t\u003c/p\u003e\u003cp\u003e\n\t\tActually, and I'm only realizing this now as I type this blog post:\n\t\tThis test is done \u003ci\u003eeven if the shots at those array elements aren't\n\t\tactive\u003c/i\u003e. So, pellets tend to be made invisible based on comparisons\n\t\twith garbage data. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\t\u003c/p\u003e\u003cp\u003e\n\t\tAnd \u003ci\u003ethen\u003c/i\u003e you notice that the \u003ci\u003eplayer shot\u003c/i\u003e\n\t\tunblit​/​move​/​render function is actually only ever called from the\n\t\t\u003ci\u003epellet\u003c/i\u003e unblit​/​move​/​render function on the one global instance\n\t\tof the player shot manager class, after pellets were unblitted. So, we\n\t\tend up with a sequence of\n\t\u003c/p\u003e\u003cblockquote\u003ePellet unblit → Pellet move → Shot unblit → Shot move → Shot render → Pellet render\u003c/blockquote\u003e\u003cp\u003e\n\t\twhich means that \u003ci\u003ewe can't ever unblit a previously rendered shot\n\t\twith a pellet\u003c/i\u003e. Sure, as terrible as this one function call is from\n\t\ta software architecture perspective, it was enough to fix this issue.\n\t\tYet we don't even get the intended positive effect, and walk away with\n\t\tpellets that are made temporarily invisible for no reason at all. So,\n\t\tuh, maybe it all just \u003ci\u003ewas\u003c/i\u003e an attempt at increasing the\n\t\tramerate on lower spec PC-98 models?\n\t\u003c/p\u003e\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tYup, that's it, we've found the most stupid piece of code in this game,\n\tperiod. It'll be hard to top this.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tI'm confident that it's possible to turn TH01 into a well-written, fluid\n\tPC-98 game, with no flickering, and no perceived lag, once it's\n\tposition-independent. With some more in-depth knowledge and documentation\n\ton the EGC (remember, there's still\n\t\u003ca href=\"/blog/2019-11-06\"\u003e📝 this one TH03 push waiting to be funded\u003c/a\u003e),\n\tyou might even be able to continue using that piece of blitter hardware.\n\tAnd no, you certainly won't need ASM micro-optimizations – just a bit of\n\tknowledge about which optimizations Turbo C++ does on its own, and what\n\tyou'd have to improve in your own code. It'd be very hard to \u003ci\u003ewrite\u003c/i\u003e\n\tworse code than what you find in TH01 itself.\n\u003c/p\u003e\u003cp\u003e\n\t(\u003cs\u003e\u003ca href=\"https://godbolt.org\"\u003eGodbolt\u003c/a\u003e for Turbo C++ 4.0J when?\u003c/s\u003e\n\tSeriously though, that would \u003ca href=\"/blog/2020-07-09\"\u003e📝 also\u003c/a\u003e be a\n\tgreat project for outside contributors!)\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tOh well. In contrast to TH04 and TH05, where 4 pushes only covered all the\n\tinvolved data types, they were enough to completely cover \u003ci\u003e all\u003c/i\u003e of\n\tthe pellet code in TH01. Everything's already decompiled, and we never\n\thave to look at it again. 😌 And with that, TH01 has also gone from by far\n\tthe least RE'd to the most RE'd game within ReC98, in just half a year! 🎉\n\t\u003cbr\u003e\n\tStill, that was enough TH01 game logic for a while.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e Next up: Making up for the delay with some\n\tmore relaxing and easy pieces of TH01 code, that hopefully make just a\n\t\u003ci\u003ebit\u003c/i\u003e more sense than all this garbage. More image formats, mainly.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-27\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-07-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-07-12T14:51:14Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-07-09",
      "url": "https://rec98.nmlgc.net/blog/2020-07-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-06-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-07-09\"\u003e\u003ctime datetime=\"2020-07-09T14:00:00Z\"\u003e2020-07-09\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/build-process\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Dealing with the difficulties of building a mixed C++/assembly project with ancient compilers.\"\u003ebuild-process\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pipeline\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Additional, helpful code generation steps, beyond regular compiler/assembler invocations.\"\u003epipeline\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTH01 pellets are coming up next, and for the first time, we'll have the\n\tchance to move hardcoded sprite data from ASM land to C land. As it would\n\tturn out, bad luck with the 2-byte alignment at the end of \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e's data segment pretty much \u003ci\u003eforces\u003c/i\u003e us to declare\n\tTH01's pellet sprites in C if we want to decompile the final few pellet\n\tfunctions without ugly workarounds for the float literals there. And while\n\tI could have just converted them into a C array and called it a day, it\n\tdid raise the question of when we are going to do this The Right And\n\tModdable Way, by auto-converting actual image files into ASM or C arrays\n\tduring the build process. These arrays are even more annoying to edit in\n\tC, after all – unlike TASM, the old C++ we have to work with doesn't\n\tsupport binary number literals, only hexadecimal or, \u003ci\u003egasp\u003c/i\u003e, octal.\n\t\u003cbr\u003e\n\tWithout the explicit funding for such a converter,\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/issues/8\"\u003eI reached out to\n\tGitHub\u003c/a\u003e, asking backers and outside contributors whether they'd be in\n\tfavor of it. As something that requires no RE skills and collides with\n\tnothing else, it would be a perfect task for C/C++ coders who want to\n\tsupport ReC98 with something other than money.\n\u003c/p\u003e\u003cp\u003e\n\tAnd surprisingly, those still exist!\n\t\u003ca href=\"https://github.com/joncampbell123\"\u003eJonathan Campbell\u003c/a\u003e, of\n\t\u003ca href=\"https://github.com/joncampbell123/dosbox-x\"\u003eDOSBox-X\u003c/a\u003e fame,\n\twent ahead and implemented all the required functionality, within just a\n\tfew days. Thanks again! The result is probably a lot more portable than it\n\twould have been if I had written it. Which is pretty relevant for future\n\tport authors – any additional tooling we write ourselves should \u003ci\u003enot\u003c/i\u003e\n\tadd to the list of problems they'll have to worry about.\n\u003c/p\u003e\u003cp\u003e\n\tRight now, all of the sprites are \u003ccode\u003e#include\u003c/code\u003ed from the big ASM\n\tdump files, which means that they have to be converted before those files\n\tare assembled during the 32-bit build part. We could have introduced a\n\tthird distinct build step there, perhaps even a 16-bit one so that we can\n\tuse Turbo C++ 4.0J to also compile the converter… However, the more\n\treasonable option was to do this at the beginning of the 32-bit build\n\tstep, and add a 32-bit Windows C++ compiler to the list of tools required\n\tfor ReC98's build process.\u003cbr\u003e\n\tAnd the best choice for ReC98 is, in fact… 🥁… the 20-year-old Borland C++\n\t5.5 freeware release.\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/README.md#building\"\u003e\n\tSee the \u003ccode\u003eREADME\u003c/code\u003e for a lengthy justification\u003c/a\u003e, as well as\n\tdownload links.\n\u003c/p\u003e\u003cp\u003e\n\tSo yes, all sprites mentioned in the GitHub issue can now be modded by\n\tsimply editing .BMP files, using an image editor of your choice. 🖌\u003cbr\u003e\n\tAnd now that that's dealt with, it's finally time for more actual\n\tprogress! TH01 pellets coming tomorrow.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-06-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-07-09T16:00:00+02:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-06-13",
      "url": "https://rec98.nmlgc.net/blog/2020-06-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-06-13\"\u003e\u003ctime datetime=\"2020-06-13T19:25:59Z\"\u003e2020-06-13 19:25\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0096\"\u003eP0096\u003c/a\u003e\n\t\t\tTH01 decompilation (.PTN format, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8ddb778...8283c5e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0097\"\u003eP0097\u003c/a\u003e\n\t\t\tTH01 decompilation (Orb physics)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/8283c5e...600f036\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0098\"\u003eP0098\u003c/a\u003e\n\t\t\tTH01 decompilation (Player shots)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/600f036...ad06748\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528, Yanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, let's finally look at some TH01 gameplay structures! The obvious\n\tchoices here are player shots and pellets, which are conveniently located\n\tin the last code segment. Covering these would therefore also help in\n\ttransferring some first bits of data in \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e from ASM\n\tland to C land. (Splitting the \u003ci\u003edata\u003c/i\u003e segment would still be quite\n\tannoying.) Player shots are immediately at the beginning…\n\u003c/p\u003e\u003cp\u003e\n\t…but wait, these are drawn as transparent sprites loaded from .PTN files.\n\tGuess we first have to spend a push on\n\t\u003ca href=\"/blog/2020-03-13\"\u003e📝 Part 2 of this format\u003c/a\u003e.\u003cbr\u003e\n\tHm, 4 functions for alpha-masked blitting and unblitting of both 16×16 and\n\t32×32 .PTN sprites that align the X coordinate to a multiple of 8\n\t(remember, the PC-98 uses a\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Planar_(computer_graphics)\"\u003eplanar\n\tVRAM memory layout\u003c/a\u003e, where 8 pixels correspond to a byte), but only one\n\tfunction that supports unaligned blitting to any X coordinate, and only\n\tfor 16×16 sprites? Which is only called twice? And doesn't come with a\n\tcorresponding unblitting function? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tYeah, \u003ci\u003e\"unblitting\"\u003c/i\u003e. TH01 isn't\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Double_buffering\"\u003edouble-buffered\u003c/a\u003e,\n\tand uses the PC-98's second VRAM page exclusively to store a stage's\n\tbackground and static sprites. Since the PC-98 has no hardware sprites,\n\tall you can do is write pixels into VRAM, and any animated sprite needs to\n\tbe manually removed from VRAM at the beginning of each frame. Not using\n\tdouble-buffering theoretically allows TH01 to simply copy back all 128 KB\n\tof VRAM once per frame to do this. \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e But that\n\twould be pretty wasteful, so TH01 just looks at all animated sprites, and\n\tselectively copies only their occupied pixels from the second to the first\n\tVRAM page.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, player shot class methods… oh, wait, the collision functions\n\tdirectly act on the Yin-Yang Orb, so we first have to spend a push on\n\t\u003ci\u003ethat\u003c/i\u003e one. And that's where the impression we got from the .PTN\n\tfunctions is confirmed: \u003ci\u003eThe orb is, in fact, only ever displayed at\n\tbyte-aligned X coordinates, divisible by 8.\u003c/i\u003e It's only thanks to the\n\tconstant spinning that its movement appears at least \u003ci\u003esomewhat\u003c/i\u003e\n\tsmooth.\u003cbr\u003e\n\tThis is purely a rendering issue; internally, its position \u003ci\u003eis\u003c/i\u003e\n\ttracked at pixel precision. Sadly, smooth orb rendering at any unaligned X\n\tcoordinate wouldn't be \u003ci\u003ethat\u003c/i\u003e trivial of a mod, because well, the\n\tnecessary functions for unaligned blitting and unblitting of 32×32 sprites\n\tdon't exist in TH01's code. Then again, there's so much potential for\n\toptimization in this code, so it might be very possible to squeeze those\n\tadditional two functions into the same C++ translation unit, even without\n\tposition independence…\n\u003c/p\u003e\u003cp\u003e\n\tMore importantly though, this was the right time to decompile the core\n\tfunctions controlling the orb physics – probably the highlight in these\n\tthree pushes for most people.\u003cbr\u003e\n\tWell, \"physics\". The X velocity is restricted to the 5 discrete states of\n\t-8, -4, 0, 4, and 8, and gravity is applied by simply adding 1 to the Y\n\tvelocity every 5 frames \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e No wonder that this can\n\teasily lead to situations in which the orb infinitely bounces from the\n\tground.\u003cbr\u003e\n\tAt least fangame authors now have\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/th01/main/player/orb.cpp\"\u003ea\n\treference of how ZUN did it originally\u003c/a\u003e, because really, this bad\n\tapproximation of physics had to have been written that way on purpose. But\n\they, it uses 64-bit floating-point variables! \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\t…sometimes at least, and quite randomly. This was also where I had to\n\tlearn about Turbo C++'s floating-point code generation, and how rigorously\n\tit defines the order of instructions when mixing \u003ccode\u003edouble\u003c/code\u003e and\n\t\u003ccode\u003efloat\u003c/code\u003e variables in arithmetic or conditional expressions.\n\tThis meant that I could only get ZUN's original instruction order by using\n\tliteral constants instead of variables, which is impossible right now\n\twithout somehow splitting the data segment. In the end, I had to resort to\n\tspelling out ⅔ of one function, and one conditional branch of another, in\n\tinline ASM. 😕 If ZUN had just written \u003ccode\u003e16.0\u003c/code\u003e instead of\n\t\u003ccode\u003e16.0f\u003c/code\u003e there, I would have saved quite some hours of my life\n\ttrying to decompile this correctly…\n\u003c/p\u003e\u003cp\u003e\n\tTo sort of make up for the slowdown in progress, here's the TH01 orb\n\tphysics debug mod I made to properly understand them. \u003cstrong\u003eEdit\n\t(2022-07-12): This mod is outdated,\n\t\u003ca href=\"/blog/2022-07-10\"\u003e📝 the current version is here\u003c/a\u003e!\u003c/strong\u003e\n\t\u003ca class=\"download\" href=\"/blog/static/2020-06-13-TH01OrbPhysicsDebug.zip?aa1368c9\" data-kb=\"110.7\"\u003e2020-06-13-TH01OrbPhysicsDebug.zip \u003c/a\u003e\n\tTo use it, simply replace \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, and run the game\n\tin debug mode, via \u003ckbd\u003egame d\u003c/kbd\u003e on the DOS prompt.\u003cbr\u003e\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/874fe6db8a1a40f978b344ed7d400bbedd7ca0a3\"\u003e\n\tIts code\u003c/a\u003e might also serve as an example of how to achieve this sort of\n\tthing without position independence.\n\u003c/p\u003e\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2020-06-13-TH01OrbPhysicsDebug.png?cb64fe7c\"\u003e\u003cimg src=\"/blog/static/2020-06-13-TH01OrbPhysicsDebug.png?cb64fe7c\" alt=\"Screenshot of the TH01 orb physics debug mod\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\u003chr\u003e\u003cp\u003e\n\tAlright, \u003ci\u003enow\u003c/i\u003e it's time for player shots though. Yeah, sure, they\n\tdon't move horizontally, so it's not \u003ci\u003etoo\u003c/i\u003e bad that those are also\n\talways rendered at byte-aligned positions. But, uh… why does this code\n\tonly use the 16×16 alpha-masked unblitting function for decaying shots,\n\tand just sloppily unblits an entire 16×16 square everywhere else?\n\u003c/p\u003e\u003cp\u003e\n\tThe worst part though: Unblitting, moving, and rendering player shots \u003ci\u003e\n\tis done in a single function, in that order\u003c/i\u003e. And that's exactly where\n\tTH01's sprite flickering comes from. Since different types of sprites are\n\tfree to overlap each other, you'd have to first unblit all types, \u003ci\u003ethen\n\t\u003c/i\u003e move all types, and \u003ci\u003ethen\u003c/i\u003e render all types, as done in later\n\tPC-98 Touhou games. If you do these three steps per-type instead, you \u003ci\u003e\n\twill\u003c/i\u003e unblit sprites of other types that have been rendered before… and\n\ttherefore end up with flicker.\u003cbr\u003e\n\tOh, and finally, ZUN also added an additional sloppy 16×16 square unblit\n\tcall if a shot collides with a pellet or a boss, for some \u003ci\u003e\n\tguaranteed\u003c/i\u003e flicker. Sigh.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tAnd that's ⅓ of all ZUN code in TH01 decompiled! Next up: Pellets!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-07-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-31\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-06-13T19:25:59Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-05-31",
      "url": "https://rec98.nmlgc.net/blog/2020-05-31",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-06-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-05-31\"\u003e\u003ctime datetime=\"2020-05-31T15:47:00Z\"\u003e2020-05-31 15:47\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0095\"\u003eP0095\u003c/a\u003e\n\t\t\tTH01 PI (Completing OP and FUUIN, .BOS pointers, scrolling)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/57a8487...8ddb778\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/thief\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN stole from others, without crediting them.\"\u003ethief\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t🎉 TH01's \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003eFUUIN.EXE\u003c/code\u003e are now fully\n\tposition-independent! 🎉\n\u003c/p\u003e\u003ch5\u003eWhat does this mean?\u003c/h5\u003e\u003cp\u003e\n\tYou can now add any data or code to TH01's main menu or ending cutscenes,\n\tby simply editing the ReC98 source, writing your mod in ASM or C++, and\n\trecompiling the code. Since all absolute memory addresses in \u003ccode\u003eOP\n\t\u003c/code\u003e and \u003ccode\u003eFUUIN\u003c/code\u003e have now been converted to labels, this\n\twill work without causing any instability. See the \u003ca href=\"/faq#pi-what\"\u003e\n\tposition independence section in the FAQ\u003c/a\u003e for a more thorough\n\texplanation about why this was a problem.\n\t\u003cbr\u003e\n\tAs an example, the most popular TH01 mod idea, replacing MDRV2 with PMD,\n\tcould now at least be \u003ci\u003eprototyped\u003c/i\u003e and \u003ci\u003etested\u003c/i\u003e in \u003ccode\u003e\n\tOP.EXE\u003c/code\u003e, without having to worry about x86 instruction lengths.\n\t\u003cbr\u003e\n\t\u003ca href=\"/blog/2019-12-29\"\u003e📝 Check the video I made for the TH04/TH05 \u003ccode\u003eOP.EXE\u003c/code\u003e PI announcement for a basic overview of how to do that.\u003c/a\u003e\n\u003ch5\u003eWhat does this not mean?\u003c/h5\u003e\u003cp\u003e\n\tThe original ZUN code hasn't been completely decompiled yet. The final\n\thigh-level parts of both the main menu and the cutscenes are still ASM,\n\twhich might make modding a bit inconvenient right now.\u003cbr\u003e\n\tIt's not that much more code though, and could quickly be covered in a few\n\tpushes if requested. Due to the plentiful monthly subscriptions, the shop\n\twill stay closed for regular orders until the end of June, but backers\n\twith outstanding contributions \u003ci\u003ecould\u003c/i\u003e request that now if they want\n\tto – simply drop me a mail. Otherwise, the \"generic TH01 RE\" money will\n\tcontinue to go towards the main game. That way, we'll have more substance\n\tto show once we \u003ci\u003edo\u003c/i\u003e decide to decompile the rest of \u003ccode\u003e\n\tOP.EXE\u003c/code\u003e and \u003ccode\u003eFUUIN.EXE\u003c/code\u003e, and likely get some press\n\tcoverage as a result.\n\u003c/p\u003e\u003chr\u003e\u003cp\u003e\n\tThen again, we've been building up to this point over the last few pushes,\n\tand it only really needed a quick look over the remaining false positives.\n\tThe majority of the time therefore went towards more PI in \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e, where the bitplane pointers for .BOS files yielded\n\tsome quite big gains. Couldn't really find any obvious reason why ZUN used\n\ttwo slighly different variations on loading and blitting those files,\n\tthough… \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tAs the final function in this rather random push, we got TH01's\n\thardware-powered scrolling function, used for screen shaking effects and\n\tthe scrolling backgrounds at the start of the Final Boss stages. And while\n\tI tried to document all these I/O writes… it turned out that ZUN actually\n\tcopied the entire function straight from the \u003ci\u003ePC-9801 Programmers'\n\tBible\u003c/i\u003e, with no changes. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e It's the \u003ccode\u003e\n\tsetgsta()\u003c/code\u003e example function on page 150. Which is terribly\n\tsuboptimal and bloated – all those integer divisions are really \u003ci\u003e\n\tnot\u003c/i\u003e how you'd write such code for a 16-bit compiler from the 90's…\n\u003c/p\u003e\u003cp\u003e\n\tAnd that gives us 60% PI overall, and 50% PI over all of TH01! Next up:\n\tMore structures… and classes, even?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-06-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-25\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-05-31T15:47:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-05-25",
      "url": "https://rec98.nmlgc.net/blog/2020-05-25",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-05-25\"\u003e\u003ctime datetime=\"2020-05-25T14:05:11Z\"\u003e2020-05-25 14:05\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0092\"\u003eP0092\u003c/a\u003e\n\t\t\tTH01 decompilation (Score menu, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/29c5a73...4403308\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0093\"\u003eP0093\u003c/a\u003e\n\t\t\tTH01 decompilation (Score menu, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4403308...0e73029\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0094\"\u003eP0094\u003c/a\u003e\n\t\t\tTH01 decompilation (Score menu, part 4 + Endings, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0e73029...57a8487\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThree pushes to decompile the TH01 high score menu… because it's\n\tcompletely terrible, and needlessly complicated in pretty much every\n\taspect:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eAnother, final set of differences between the \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e\n\tand \u003ccode\u003eFUUIN.EXE\u003c/code\u003e versions of the code. Which are so\n\tinsignificant that it \u003ci\u003emust\u003c/i\u003e mean that ZUN kept this code in two\n\tseparate, manually and imperfectly synced files. The \u003ccode\u003eREIIDEN.EXE\n\t\u003c/code\u003e version, only shown when game-overing, automatically jumps to the\n\tenter/\u003cspan lang=\"ja\"\u003e終\u003c/span\u003e button after the 8th character was entered,\n\tand also has a completely invisible timeout that force-enters a high score\n\tname after 1000… \u003ci\u003ekey presses\u003c/i\u003e? Not frames? Why. Like, how do you\n\teven realistically such a number. (Best guess: It's a hidden easter egg to\n\tamuse players who place drinking glasses on cursor keys. Or beer bottles.)\n\t\u003cbr\u003e\n\tThat's all the differences that are \u003ci\u003emaybe\u003c/i\u003e visible if you squint\n\thard enough. On top of that though, we got a bunch of further, minor code\n\torganization differences that serve no purpose other than to waste\n\tdecompilation time, and certainly did their part in stretching this out to\n\t3 pushes instead of 2.\u003c/li\u003e\n\u003c/ul\u003e\u003cul\u003e\n\t\u003cli\u003eEntered names are restricted to a set of 16-bit, full-width Shift-JIS\n\tcodepoints, yet are still accessed as 8-bit byte arrays everywhere. This\n\tbloats both the C++ and generated ASM code with needless byte splits,\n\tswaps, and bit shifts. Same for the route kanji. You have this 16-, heck,\n\teven 32-bit CPU, why not use it?! (Fun fact: \u003ccode\u003eFUUIN.EXE\u003c/code\u003e is\n\texplicitly compiled for a 80186, for the most part – unlike \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e, which \u003ci\u003edoes\u003c/i\u003e use Turbo C++'s 80386 mode.)\u003c/li\u003e\n\u003c/ul\u003e\u003cul\u003e\n\t\u003cli\u003eThe sensible way of storing the current position of the alphabet\n\tcursor would simply be two variables, indicating the logical row and\n\tcolumn inside the character map. When rendering, you'd then transform\n\tthese into screen space. This can keep the on-screen position constants in\n\ta single place of code.\u003cbr\u003e\n\tTH01 does the opposite: The selected character is stored directly in terms\n\tof its on-screen position, which is then mapped \u003ci\u003eback\u003c/i\u003e to a character\n\tindex for every processed input and the subsequent screen update. There's\n\tno notion of a logical row or column anywhere, and consequently, the\n\tposition constants are vomited all over the code.\u003c/li\u003e\n\u003c/ul\u003e\u003cul\u003e\n\t\u003cli\u003eWhich might not be \u003ci\u003eas\u003c/i\u003e bad if the character map had a uniform\n\tgrid structure, with no gaps. But the one in TH01 looks like this:\n\t\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2020-05-25-alphabet.png?8e60fac9\"\u003e\u003cimg src=\"/blog/static/2020-05-25-alphabet.png?8e60fac9\" alt=\"TH01 high score name character map\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\n\tAnd with no sense of abstraction anywhere, both input handling and\n\trendering end up with a separate \u003ccode\u003eif\u003c/code\u003e branch for at least 4 of\n\tthe 6 rows.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIn the end, I just gave up with my usual redundancy reduction efforts for\n\tthis one. Anyone wanting to change TH01's high score name entering code\n\twould be better off just rewriting the entire thing properly.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's all of the shared code in TH01! Both \u003ccode\u003eOP.EXE\u003c/code\u003e and\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e are now only missing the actual main menu and\n\tending code, respectively. Next up, though: The long awaited TH01 PI push.\n\tWhich will not only deliver 100% PI for \u003ccode\u003eOP.EXE\u003c/code\u003e and \u003ccode\u003e\n\tFUUIN.EXE\u003c/code\u003e, but also probably \u003ci\u003equite\u003c/i\u003e some gains in \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e. With now over 30% of the game decompiled, it's about\n\ttime we get to look at some gameplay code!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-31\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-05-25T14:05:11Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-05-12",
      "url": "https://rec98.nmlgc.net/blog/2020-05-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-05-12\"\u003e\u003ctime datetime=\"2020-05-12T14:07:25Z\"\u003e2020-05-12 14:07\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0090\"\u003eP0090\u003c/a\u003e\n\t\t\tTH01 decompilation (Input blockers + Input, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/90252cc...07dab29\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0091\"\u003eP0091\u003c/a\u003e\n\t\t\tTH01 decompilation (Input, part 2 + Score menu, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/07dab29...29c5a73\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/input\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Processing data entered from the keyboard or a joypad.\"\u003einput\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bug\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that cause catastrophic effects when given malformed input. Modders beware!\"\u003ebug\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBack to TH01, and its high score menu… oh, wait, that one will eventually\n\tinvolve keyboard input. And thanks to the generous TH01 funding situation,\n\tthere's really no reason \u003ci\u003enot\u003c/i\u003e to cover that right now. After all,\n\tTH01 is the last game where input still hadn't been RE'd.\u003cbr\u003e\n\tBut first, let's also cover that one unused blitting function, together\n\twith \u003ccode\u003eREIIDEN.CFG\u003c/code\u003e loading and saving, which are in front of\n\tthe input function in \u003ccode\u003eOP.EXE\u003c/code\u003e… (By now, we all know about\n\t\u003ca href=\"https://tcrf.net/Touhou_Reiiden:_The_Highly_Responsive_to_Prayers#Bomb_Option\"\u003e\n\tthe hidden start bomb configuration\u003c/a\u003e, right?)\n\u003c/p\u003e\u003cp\u003e\n\tUnsurprisingly, the earliest game also implements input in the messiest\n\tway, with a different function for each of the three executables. \"Because\n\tthey all react differently to keyboard inputs \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\",\n\tapparently? \u003ccode\u003eOP.EXE\u003c/code\u003e even has two functions for it, one for the\n\t\u003cspan style=\"color: #993399;\"\u003eSTART / CONTINUE / OPTION / QUIT\u003c/span\u003e main\n\tmenu, and one for both Option and Music Test menus, both of which directly\n\tperform the ring arithmetic on the menu cursor variable. A consistent\n\tseparation of keyboard polling from input processing apparently wasn't all\n\ttoo obvious of a thought, since it's only truly done from TH02 on.\n\u003c/p\u003e\u003cp\u003e\n\tThis lack of proper architecture becomes actually hilarious once you\n\tnotice that it did in fact facilitate a recursion bug!\n\t\u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\n\t\u003ca href=\"https://tcrf.net/Touhou_Reiiden:_The_Highly_Responsive_to_Prayers#Debug_Features\"\u003e\n\tIn case you've been living under a rock for the past 8 years, TH01 shipped\n\twith debugging features, which you can enter by running the game via\n\t\u003ccode\u003egame d\u003c/code\u003e from the DOS prompt.\u003c/a\u003e These features include a\n\tmemory info screen, shown when pressing PgUp, implemented as one blocking\n\tfunction (\u003ccode\u003etest_mem()\u003c/code\u003e) called directly in response to the\n\tpressed key inside the polling function. \u003ccode\u003etest_mem()\u003c/code\u003e only\n\treturns once that screen is left by pressing PgDown. And in order to poll\n\tinput… it directly calls back into the same polling function that called\n\tit in the first place, after a 3-frame delay.\n\u003c/p\u003e\u003cp\u003e\n\tWhich means that \u003ci\u003ethis screen is actually re-entered for every 3 frames\n\tthat the PgUp key is being held\u003c/i\u003e. And yes, you can, of course, also\n\tcrash the system via a stack overflow this way by holding down PgUp for a\n\tfew seconds, if that's your thing.\u003cbr\u003e\n\t\u003cstrong\u003eEdit (2020-09-17):\u003c/strong\u003e Here's a video from\n\t\u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e, showing off this\n\texact stack overflow crash while running under the\n\t\u003ca href=\"https://www.vector.co.jp/soft/dos/hardware/se025675.html\"\u003eVEM486\n\tmemory manager\u003c/a\u003e, which displays additional information about these\n\tsorts of crashes:\n\t\u003cscript\u003e\n\t\texternalRegister('2020-05-12', 'vid', 'https://youtube.com/embed/8V7H6PaTUbU');\n\t\u003c/script\u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ciframe id=\"2020-05-12-vid\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tWhat makes this even funnier is that the code actually tracks the last\n\tstate of every polled key, to prevent exactly that sort of bug. But the\n\tcopy-pasted assignment of the last input state is only done \u003ci\u003eafter\u003c/i\u003e\n\t\u003ccode\u003etest_mem()\u003c/code\u003e already returned, making it effectively pointless\n\tfor PgUp. It \u003ci\u003edoes\u003c/i\u003e work as intended for PgDown… and that's why you\n\thave to actually press \u003ci\u003eand release\u003c/i\u003e this key once for every call to\n\t\u003ccode\u003etest_mem()\u003c/code\u003e in order to actually get back into the game. Even\n\tthough a single call to PgDown will already \u003ci\u003eshow\u003c/i\u003e the game screen\n\tagain.\n\u003c/p\u003e\u003cp\u003e\n\tIn maybe more relevant news though, this function also came with what can\n\tbe considered the first piece of actual gameplay logic! Bombing via\n\tdouble-tapping the Z and X keys is also handled here, and now we know that\n\tboth keys simply have to be tapped twice within a window of 20 frames.\n\tThey are tracked independently from each other, so you don't necessarily\n\thave to press them simultaneously.\u003cbr\u003e\n\tIn debug mode, the \u003ccode\u003ebomb\u003c/code\u003e count tracks precisely this window of\n\ttime. That's why it only resets back to 0 when pressing Z or X if it's\n\t≥20.\n\u003c/p\u003e\u003cp\u003e\n\tSure, TH01's code is expectedly terrible and messy. But compared to the\n\tmicro-optimizations of TH04 and TH05, it's an absolute joy to work on, and\n\topening all these ZUN bug loot boxes is just the icing on the cake.\n\tLooking forward to more of the high score menu in the next pushes!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-25\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-05-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-05-12T14:07:25Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-05-04",
      "url": "https://rec98.nmlgc.net/blog/2020-05-04",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-04-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-05-04\"\u003e\u003ctime datetime=\"2020-05-04T14:16:57Z\"\u003e2020-05-04 14:16\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0088\"\u003eP0088\u003c/a\u003e\n\t\t\tTH04/TH05 PI (Stage enemy structure)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/97ce7b7...da6b856\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0089\"\u003eP0089\u003c/a\u003e\n\t\t\tTH04/TH05 decompilation (Stage and BGM title popups)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/da6b856...90252cc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, [Anonymous], Blue Bolt\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/enemy\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Common stage enemies with simple scripts, in contrast to midbosses or bosses.\"\u003eenemy\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/portability\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Notes about future ports of the games away from x86 and the PC-98 platform.\"\u003eportability\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAs expected, we've now got the TH04 and TH05 stage enemy structure,\n\tfinishing position independence for all big entity types. This one was\n\tquite straightfoward, as the .STD scripting system is pretty simple.\n\u003c/p\u003e\u003cp\u003e\n\tIts most interesting aspect can be found in the way timing is handled. In\n\tWindows Touhou, all .ECL script instructions come with a frame field that\n\tdefines when they are executed. In TH04's and TH05's .STD scripts, on the\n\tother hand, it's up to each individual instruction to add a frame time\n\tparameter, anywhere in its parameter list. This frame time defines for how\n\tlong this instruction should be repeatedly executed, before it manually\n\tadvances the instruction pointer to the next one. From what I've seen so\n\tfar, these instruction typically apply their effect on the first frame\n\tthey run on, and then do nothing for the remaining frames.\u003cbr\u003e\n\tOh, and you can't nest the \u003ccode\u003eLOOP\u003c/code\u003e instruction, since the enemy\n\tstructure only stores one single counter for the current loop iteration.\n\u003c/p\u003e\u003cp\u003e\n\tJust from the structure, the only innovation introduced by TH05 seems to\n\thave been enemy subtypes. These can be used to parametrize scripts via\n\tconditional jumps based on this value, as a first attempt at cutting down\n\tthe need to duplicate entire scripts for similar enemy behavior. And\n\tthanks to TH05's favorable segment layout, this game's version of the\n\t.STD enemy script interpreter is even immediately ready for decompilation,\n\tin one single future push.\n\u003c/p\u003e\u003cp\u003e\n\tAs far as I can tell, that now only leaves\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e.MPN file loading\u003c/li\u003e\n\t\u003cli\u003eplayer bomb animations\u003c/li\u003e\n\t\u003cli\u003esome structures specific to the Shinki and EX-Alice battles\u003c/li\u003e\n\t\u003cli\u003eplus some smaller things I've missed over the years\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tuntil TH05's \u003ccode\u003eMAIN.EXE\u003c/code\u003e is completely position-independent.\n\t\u003cbr\u003e\n\tWhich, however, won't be all it needs for that 100% PI rating on the front\n\tpage. And with that many false positives, it's quite easy to get lost with\n\timmediately reverse-engineering everything around them. This time, the\n\trendering of the text dissolve circles, used for the stage and BGM title\n\tpopups, caught my eye…  and since the high-level code to handle all of\n\tthat was near the end of a segment in both TH04 and TH05, I just decided\n\tto immediately decompile it all. Like, how hard could it possibly be?\n\tSure, it needed another segment split, which was a \u003ci\u003ebit\u003c/i\u003e harder due\n\tto all the existing ASM referencing code in that segment, but certainly\n\tnot impossible…\n\u003c/p\u003e\u003cp\u003e\n\tOh wait, this code depends on 9 other sets of identifiers that haven't\n\tbeen declared in C land before, some of which require vast reorganizations\n\tto bring them up to current consistency standards. Whoops! Good thing that\n\tthis is the part of the project I'm still offering for free…\u003cbr\u003e\n\tAmong the referenced functions was \u003ccode\u003etiles_invalidate_around()\u003c/code\u003e,\n\twhich marks the stage background tiles within a rectangular area to be\n\tredrawn this frame. And this one must have had the hardest function\n\tsignature to figure out in all of PC-98 Touhou, because \u003ci\u003eit actually\n\tseems impossible\u003c/i\u003e. Looking at all the ways the game passes the center\n\tcoordinate to this function, we have\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003eX and Y as 16-bit integer literals, merged into a single\n\t\u003ccode\u003ePUSH\u003c/code\u003e of a 32-bit immediate\u003c/li\u003e\n\t\u003cli\u003eX and Y calculated and pushed independently from each other\u003c/li\u003e\n\t\u003cli\u003eby-value copies of entire \u003ccode\u003ePoint\u003c/code\u003e instances\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\tAny single declaration would only lead to at most two of the three cases\n\tgenerating the original instructions. No way around separately declaring\n\tthe function in every translation unit then, with the correct parameter\n\tlist for the respective calls. That's how ZUN must have also written it.\n\u003c/p\u003e\u003cp\u003e\n\tOh well, we would have needed to do all of this \u003ci\u003esome\u003c/i\u003e time. At least\n\tthere were quite a bit of insights to be gained from the actual\n\tdecompilation, where using \u003ccode\u003econst\u003c/code\u003e references actually made it\n\tpossible to turn quite a number of potentially ugly macros into wholesome\n\t\u003ccode\u003einline\u003c/code\u003e functions.\n\u003c/p\u003e\u003cp\u003e\n\tBut still, TH04 and TH05 will come out of ReC98's decompilation as one big\n\tmess. A lot of further manual decompilation and refactoring, beyond the\n\tlimits of the original binary, would be needed to make these games\n\tportable to any non-PC-98, non-x86 architecture.\u003cbr\u003e\n\tAnd yes, that includes IBM-compatible DOS – which, for some reason, a\n\tnumber of people see as the obvious choice for a first system to port\n\tPC-98 Touhou to. This will barely be easier. Sure, you'll save the effort\n\tof decompiling all the remaining original ASM. But even \u003ci\u003ewith\u003c/i\u003e\n\tmaster.lib's \u003ccode\u003eMASTER_DOSV\u003c/code\u003e setting, these games still very much\n\trely on PC-98 hardware, with corresponding assumptions all over ZUN's\n\tcode. You \u003ci\u003ewill\u003c/i\u003e need to provide abstractions for the PC-98's\n\tsuperimposed text mode, the gaiji, and planar 4-bit color access in\n\tgeneral, exchanging the use of the PC-98's GRCG and EGC blitter chips with\n\tsomething else. At that point, you might as well port the game to one\n\tgeneric 640×400 framebuffer and away from the constraints of DOS,\n\tresulting in that Doom source code-like situation which made \u003ci\u003ethat\u003c/i\u003e\n\tgame easily portable to every architecture to begin with. But ZUN just\n\twasn't a John Carmack, sorry.\n\u003c/p\u003e\u003cp\u003e\n\tOr what do I know. I've never programmed for IBM-compatible DOS, but maybe\n\tReC98's audience \u003ci\u003edoes\u003c/i\u003e include someone who is intimately familiar\n\twith IBM-compatible DOS so that the constraints aren't much of an issue\n\tfor them? But even then, 16-bit Windows would make \u003ci\u003emuch\u003c/i\u003e more sense\n\tas a first porting target if you don't want to bother with that\n\tundecompilable ASM.\n\u003c/p\u003e\u003cp\u003e\n\tAt least I won't have to look at TH04 and TH05 for quite a while now.\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e The delivery delays have made it obvious that\n\tmy life has become pretty busy again, probably until September. With a\n\ttotal of 9 TH01 pushes from monthly subscriptions now waiting in the\n\tbacklog, the shop will stay closed until I've caught up with most of\n\tthese. Which I'm quite hyped for!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-04-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-05-04T14:16:57Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-04-16",
      "url": "https://rec98.nmlgc.net/blog/2020-04-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-04-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-04-16\"\u003e\u003ctime datetime=\"2020-04-16T13:09:16Z\"\u003e2020-04-16 13:09\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0086\"\u003eP0086\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Random PI blockers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/54ee99b...24b96cd\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0087\"\u003eP0087\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Score popup numbers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/24b96cd...97ce7b7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], Blue Bolt, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuuka-6\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH04\u0026#39;s Stage 6 boss.\"\u003eyuuka-6\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAlright, the score popup numbers shown when collecting items or defeating\n\t(mid)bosses. The second-to-last remaining big entity type in TH05… with\n\tquite some PI false positives in the memory range occupied by its data.\n\tGood thing I still got some outstanding generic RE pushes that haven't\n\tbeen claimed for anything more specific in over a month! These\n\tconveniently allowed me to RE most of these functions right away, the\n\tright way.\n\u003c/p\u003e\u003cp\u003e\n\tMost of the false positives were boss HP values, passed to a \"boss phase\n\tend\" function which sets the HP value at which the next phase should end.\n\tStage 6 Yuuka, Mugetsu, and EX-Alice have their own copies of this\n\tfunction, in which they also reset certain boss-specific global variables.\n\tSince I always like to cover all varieties of such duplicated functions at\n\tonce, it made sense to reverse-engineer all the involved variables while I\n\twas at it… and that's why this was exactly the right time to cover the\n\timplementation details of Stage 6 Yuuka's parasol and vanishing animations\n\tin TH04. \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tWith still a bit of time left in that RE push afterwards, I could also\n\tstart looking into some of the smaller functions that didn't quite fit\n\tinto other pushes. The most notable one there was a simple function that\n\taims from any point to the current player position. Which actually only\n\tbecame a separate function in TH05, probably since it's called 27 times in\n\ttotal. That's 27 places no longer being blocked from further RE progress.\n\u003c/p\u003e\u003cp\u003e\n\t\u003ca href=\"https://github.com/wintiger0222/ReC98\"\u003eWindowsTiger\u003c/a\u003e already\n\tdid most of the work for the score popup numbers in January, which meant\n\tthat I only had to review it and bring it up to ReC98's current coding\n\tstyles and standards. This one turned out to be one of those rare features\n\twhose TH05 implementation is significantly \u003ci\u003eless\u003c/i\u003e insane than the\n\tTH04 one. Both games lazily redraw only the tiles of the stage background\n\tthat were drawn over in the previous frame, and try their best to minimize\n\tthe amount of tiles to be redrawn in this way. For these popup numbers,\n\tthis involves calculating the on-screen width, based on the exact number\n\tof digits in the point value. TH04 calculates this width every frame\n\tduring the rendering function, and even resorts to setting that field\n\tthrough the digit iteration pointer via self-modifying code… yup. TH05, on\n\tthe other hand, simply calculates the width once when spawning a new popup\n\tnumber, during the conversion of the point value to\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\"\u003ebinary-coded\n\tdecimal\u003c/a\u003e. The \"×2\" multiplier suffix being removed in TH05 certainly\n\talso helped in simplifying that feature in this game.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's ⅓ of TH05 reverse-engineered! Next up, one more TH05 PI push,\n\tin which the stage enemies hopefully finish all the big entity types.\n\tMaybe it will also be accompanied by another RE push? In any case, that\n\twill be the last piece of TH05 progress for quite some time. The next TH01\n\tstretch will consist of 6 pushes at the very least, and I currently have\n\tno idea of how much time I can spend on ReC98 a month from now…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-05-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-04-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-04-16T13:09:16Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-04-03",
      "url": "https://rec98.nmlgc.net/blog/2020-04-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-04-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-04-03\"\u003e\u003ctime datetime=\"2020-04-03T15:46:03Z\"\u003e2020-04-03 15:46\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0085\"\u003eP0085\u003c/a\u003e\n\t\t\tTH02/TH04/TH05 RE (Pellet rendering + TH04/TH05 gather circles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/110d6dd...54ee99b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/good-code\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWait, PI for \u003ccode\u003eFUUIN.EXE\u003c/code\u003e is mainly blocked by the high score\n\tmenu? That one should \u003ci\u003ereally\u003c/i\u003e be properly decompiled in a separate\n\tRE push, since it's also present in largely identical form in\n\t\u003ccode\u003eREIIDEN.EXE\u003c/code\u003e… but I currently lack the explicit funding to do\n\tthat.\n\u003c/p\u003e\u003cp\u003e\n\tAnd as it turns out, I shouldn't really capture any of the existing generic\n\tRE contributions for it either. Back in 2018 when I ran the crowdfunding\n\ton the \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e Discord server, I said that generic RE\n\tcontributions would never go towards TH01. No one was interested in that\n\tgame back then, and as it's significantly different from all the other\n\tgames, it made sense to only cover it if explicitly requested.\u003cbr\u003e\n\tAs \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e still remains one of the biggest supporters and\n\tadvertisers for ReC98, someone recently believed that this rule was still\n\tin effect, despite not being mentioned anywhere on this website.\n\u003c/p\u003e\u003cp\u003e\n\tFast forward to today, and TH01 has become the single most supported game\n\tlately, with plenty of incomplete pushes still open to be completed.\n\tReverse-engineering it has proven to be quite efficient, yielding lots of\n\tcompletion percentage points per push. This, I suppose, is exactly what\n\tbackers that don't give any specific priorities are mainly interested in.\n\tTherefore, \u003ci\u003eI \u003cstrong\u003ewill\u003c/strong\u003e allocate future partial\n\tcontributions to TH01, whenever it makes sense\u003c/i\u003e.\n\u003c/p\u003e\u003cp\u003e\n\tSo, instead of rushing TH01 PI, let's wait for Ember2528's\n\tApril subscription, and get the 25% total RE milestone with some TH05 PI\n\tprogress instead. This one primarily focused on the gather circles\n\t(spirals…?), the third-last missing entity type in TH05. These are\n\trendered using the same 8×8 pellet sprite introduced in TH02… except that\n\tthe \u003ci\u003eactual\u003c/i\u003e pellets received a darkened bottom part in TH04\n\t\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAgMAAAC5YVYYAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAAlQTFRFAAAAqqr/////38h8nAAAAAF0Uk5TAEDm2GYAAAAeSURBVAjXY+BawKC1gmHVKoZVK0EoaymDaAgDawAAXhcHRDMdG+8AAAAASUVORK5CYII=\"\u003e.\n\tWhich, in turn, is actually rendered quite efficiently – the games first\n\trender the top white part of all pellets, followed by the bottom gray part\n\tof all pellets. The PC-98 GRCG is used throughout the process, doing its\n\ttypical job of accelerating monochrome blitting, and by arranging the\n\trendering like this, only two GRCG color changes are required to draw any\n\tnumber of pellets. I \u003ci\u003eguess\u003c/i\u003e that makes it quite a worthwhile\n\toptimization? Don't ask me for specific performance numbers or even saved\n\tcycles, though \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up, one more TH05 PI push!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-04-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-04-03T15:46:03Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-03-22",
      "url": "https://rec98.nmlgc.net/blog/2020-03-22",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-04-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-03-22\"\u003e\u003ctime datetime=\"2020-03-22T09:18:27Z\"\u003e2020-03-22 09:18\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0084\"\u003eP0084\u003c/a\u003e\n\t\t\tTH01 decompilation (REYHI*.DAT loading and creation)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dfac2f2...110d6dd\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tFinal TH01 RE push for the time being, and as expected, we've got the\n\tsuperficially final piece of shared code between the TH01 executables.\n\tHowever, just having a single implementation for loading and recreating\n\tthe \u003ccode\u003eREYHI*.DAT\u003c/code\u003e score files would have been way above ZUN's\n\tstandards of consistency. So ZUN had the unique idea to mix up the file\n\tI/O APIs, using master.lib functions in \u003ccode\u003eREIIDEN.EXE\u003c/code\u003e, and\n\tPOSIX functions (along with error messages and disabled interrupts) in\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e… \u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Could have been worse\n\tthough, as it was possible to abstract that away quite nicely.\n\u003c/p\u003e\u003cp\u003e\n\tThat code wasn't quite in the natural way of decompilation either. As it\n\tturns out though, \u003ca href=\"/blog/2019-09-21\"\u003e📝 segment splitting\u003c/a\u003e isn't\n\tso painful after all if one of the new segments only has a few functions.\n\tDefinitely going to do that more often from now on, since it allows a much\n\tlarger number of functions to be immediately decompiled. Which is always\n\tsuperior to somehow transforming a function's ASM into a form that I can\n\tconfidently call \"reverse-engineered\", only to revisit it again later for\n\tits decompilation.\n\u003c/p\u003e\u003cp\u003e\n\tAnd while I unfortunately missed 25% of total RE by a bit, this push\n\treached two other and perhaps even more significant milestones:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ci\u003e\u003ca href=\"/blog/2019-09-09\"\u003e📝 In a little over 6 months\u003c/a\u003e, we've\n\t\u003cstrong\u003edoubled\u003c/strong\u003e the amount of reverse-engineered PC-98 Touhou\n\tgame code!\u003c/i\u003e 📈\u003c/li\u003e\n\t\u003cli\u003eAfter (finally) compressing all unknown parts of the BSS segments\n\tusing arrays, the number of remaining lines in the \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e ASM dump has fallen below TASM's limit of 65,535. Which\n\tmeans that we no longer need that annoying \u003ccode\u003eth01_reiiden_2.inc\u003c/code\u003e\n\tfile that everyone has forgotten about at least once.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up, PI milestones!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-04-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-03-22T09:18:27Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-03-18",
      "url": "https://rec98.nmlgc.net/blog/2020-03-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-03-18\"\u003e\u003ctime datetime=\"2020-03-18T19:39:32Z\"\u003e2020-03-18 19:39\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0083\"\u003eP0083\u003c/a\u003e\n\t\t\tTH01 decompilation (.PTN format, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f6cbff0...dfac2f2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/good-code\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN implemented surprisingly well.\"\u003egood-code\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNope, RL has given me \u003ci\u003eplenty\u003c/i\u003e of things to do from home after all,\n\tso the current cap still remains an accurate representation of my free\n\ttime. 😕\n\u003c/p\u003e\u003cp\u003e\n\tFor now though, we've got one more TH01 file format push, covering the\n\tcore functions for loading and displaying the 32×32 and 16×16 sprites from\n\tthe .PTN files, as announced – and probably one of the last ones for quite\n\ta while to yield both RE and PI progress way above average. But what is\n\tthis, error return values in a ZUN game?! And \u003ci\u003eactually good code\u003c/i\u003e\n\tfor deriving the alpha channel from the 16th color in the hardware\n\tpalette?! Sure, the rest of the code could still be improved a lot, but\n\tthat was quite a surprise, especially after the spaghetti code of\n\t\u003ca href=\"/blog/2020-03-13\"\u003e📝 the last push\u003c/a\u003e. That makes up for two of\n\tthe .PTN structure fields (one of them always 0, and one of them always 1)\n\tremaining unused, and therefore unknown.\n\u003c/p\u003e\u003cp\u003e\n\tZUN also uses the .PTN image slots to store the background of frequently\n\tupdated VRAM sections, in order to be able to repeatedly draw on top of\n\tthem – like for example the HUD area where the score and time numbers are\n\tdrawn. Future games would simply use the text RAM and gaiji for those\n\tnumbers. This would have worked just fine for TH01 too – especially since\n\tall the functions decompiled so far align the VRAM X coordinate to the\n\t8-pixel byte grid, which is the simplest way of accessing VRAM given the\n\tPC-98's\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Planar_(computer_graphics)\"\u003eplanar\n\tmemory layout\u003c/a\u003e. Looks as if ZUN simply wasn't aware of gaiji during the\n\tdevelopment of TH01.\n\u003c/p\u003e\u003cp\u003e\n\tThis won't be the last time I cover the .PTN format, since all the\n\tblitting functions that actually \u003ci\u003euse\u003c/i\u003e alpha are exclusive to \u003ccode\u003e\n\tREIIDEN.EXE\u003c/code\u003e, and currently out of decompilation reach. But after\n\tsome more long overdue cleaning work, TH01 has now passed both TH02 and\n\teven TH04 to become \u003ci\u003ethe second-most reverse-engineered game in\n\tall of ReC98, in terms of absolute numbers\u003c/i\u003e! 🎉\n\u003c/p\u003e\u003cp\u003e\n\tAlso, PI for TH01's \u003ccode\u003eOP.EXE\u003c/code\u003e is imminent. Next up though, we've\n\tfirst got the probably final double-speed push for TH01, covering the last\n\tset of duplicated functions between the three binaries – quite fitting for\n\tthe currently last fully funded, outstanding TH01 RE push. Then, we also\n\t\u003ci\u003emight\u003c/i\u003e get \u003ccode\u003eFUUIN.EXE\u003c/code\u003e PI within the same push\n\tafterwards? After that, TH01 progress \u003ci\u003ewill\u003c/i\u003e be slowing down, since\n\tI'd then have to cover \u003ci\u003eeither\u003c/i\u003e the main menu \u003ci\u003eor\u003c/i\u003e in-game code\n\t\u003ci\u003eor\u003c/i\u003e the cutscenes, depending on what the backers request. (By\n\tdefault, it's going to be in-game code, of course.)\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-03-18T19:39:32Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-03-13",
      "url": "https://rec98.nmlgc.net/blog/2020-03-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-03-13\"\u003e\u003ctime datetime=\"2020-03-13T18:56:50Z\"\u003e2020-03-13 18:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0082\"\u003eP0082\u003c/a\u003e\n\t\t\tTH01 decompilation (.GRP format)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5ac9b30...f6cbff0\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/thief\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN stole from others, without crediting them.\"\u003ethief\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tLast of the 3 weeks of almost full-time ReC98 work, supposedly the least\n\tstressful one, and then things still get delayed thanks to illness 😕 In\n\tbetter news though, it looks like I'll be able to extend these 3 weeks to\n\t\u003ci\u003e8\u003c/i\u003e, as my RL is shutting down for coronavirus reasons. I'm going to\n\twait a bit for the dust to settle before raising the crowdfunding cap\n\tthough, since RL might give me more to do from home after all. I may or\n\tmay not also get commissioned for a non-Touhou translation patch project\n\tto be worked on in that time…\n\u003c/p\u003e\u003cp\u003e\n\tThe .GRP file functions turned out to, of course, also be present in\n\t\u003ccode\u003eFUUIN.EXE\u003c/code\u003e. In fact, that binary had the largest share of\n\tprogress in this push, since it's the only one to include \u003ci\u003eanother\u003c/i\u003e\n\treimplementation of master.lib-style hardware palette fading. As a typical\n\tlittle ZUN inconsistency, the \u003ccode\u003eFUUIN.EXE\u003c/code\u003e version of one .GRP\n\tpalette function directly calls one of these functions.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the functions themselves, they basically wrap the single-function\n\t\u003ca href=\"https://www.vector.co.jp/soft/dos/prog/se037608.html\"\u003ePi load and\n\tdisplay library by 電脳科学研究所/BERO\u003c/a\u003e in a bowl of global state\n\tspaghetti. 🍝 At least the function names now clearly encode important\n\tside effects like, y'know, a changed hardware palette. The reason ZUN used\n\tthis separate library over master.lib's PI loading functions was probably\n\tits support for defining a color as transparent. This feature is used for\n\tthe red box in the main menu, and the large cyan Siddhaṃ seed syllables in\n\t(again) the Konngara fight.\n\u003c/p\u003e\u003cp\u003e\n\tNext up, we've got the .PTN format!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-07\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-03-13T18:56:50Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-03-07",
      "url": "https://rec98.nmlgc.net/blog/2020-03-07",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-03-07\"\u003e\u003ctime datetime=\"2020-03-07T20:45:20Z\"\u003e2020-03-07 20:45\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0081\"\u003eP0081\u003c/a\u003e\n\t\t\tTH01 decompilation (.GRZ format)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0252da2...5ac9b30\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eEmber2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/konngara\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 20 boss, on the 地獄/Jigoku route.\"\u003ekonngara\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/contribution-ideas\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Potential improvements that volunteers could meaningfully contribute to this project. Mostly minor issues, or slightly out of scope of the main project.\"\u003econtribution-ideas\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSadly, we've already reached the end of fast triple-speed TH01 progress\n\twith \u003ca href=\"/blog/2020-03-03\"\u003e📝 the last push\u003c/a\u003e, which decompiled the\n\tlast segment shared by all three of TH01's executables. There's still a\n\tbit of double-speed progress left though, with a small number of code\n\tsegments that are shared between just two of the three executables.\n\u003c/p\u003e\u003cp\u003e\n\tAt the end of the first one of these, we've got all the code for the .GRZ\n\tformat – which is yet another run-length encoded image format, but this\n\ttime storing up to 16 full 640×400 16-color images with an alpha bit. This\n\tone is exclusively used to wastefully store Konngara's sword slash and\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Kuji-in\"\u003ekuji-in\u003c/a\u003e kill\n\tanimations. Due to… suboptimal code organization, the code for the format\n\tis also present in \u003ccode\u003eOP.EXE\u003c/code\u003e, despite not being used there. But\n\they, that brings TH01 to over 20% in RE!\n\u003c/p\u003e\u003cp\u003e\n\tDecoupling the RLE command stream from the pixel data sounds like a nice\n\tidea at first, allowing the format to efficiently encode a variety of\n\tanimation frames displayed all over the screen… \u003ci\u003eif ZUN actually made\n\tuse of it\u003c/i\u003e. The RLE stream also has quite some ridiculous overhead,\n\tstarting with 1 byte to store the 1-bit command (putting a single 8×1\n\tpixel block, or entering a run of N such blocks). Run commands then store\n\tanother 1-byte run length, which has to be followed by \u003ci\u003eanother\u003c/i\u003e\n\tcommand byte to identify the run as putting N blocks, or skipping N blocks.\n\tAnd the pixel data is just a sequence of these blocks for all 4 bitplanes,\n\tin uncompressed form…\n\u003c/p\u003e\u003cp\u003e\n\tAlso, have some rips of all the images this format is used for:\n\u003c/p\u003e\u003cfigure class=\"side_by_side small\"\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-0.png?8738d615\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-0.png?8738d615\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 1/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-1.png?1dc7e625\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-1.png?1dc7e625\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 2/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-2.png?887bdee0\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-2.png?887bdee0\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 3/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-3.png?1493f77c\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-3.png?1493f77c\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 4/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-4.png?11a87c1d\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-4.png?11a87c1d\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 5/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-5.png?ba59da94\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-5.png?ba59da94\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 6/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-6.png?4d3415e4\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-6.png?4d3415e4\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 7/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-7.png?1333c2fd\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-7.png?1333c2fd\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 8/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-8.png?0357da2a\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-8.png?0357da2a\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 9/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-9.png?7b46fb8d\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-9.png?7b46fb8d\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 10/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-A.png?a95e9b7b\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-A.png?a95e9b7b\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 11/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-B.png?909e4d3f\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-B.png?909e4d3f\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 12/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-C.png?3d31dc12\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-C.png?3d31dc12\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 13/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-D.png?f9f4e64a\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-D.png?f9f4e64a\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 14/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-E.png?2d616e01\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-E.png?2d616e01\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 15/16\"\n\t\u003e\u003c/a\u003e\u003ca href=\"/blog/static/2020-03-07-boss8.grz-F.png?c943b8db\"\u003e\u003cimg\n\t\tsrc=\"/blog/static/2020-03-07-boss8.grz-F.png?c943b8db\"\n\t\talt=\"\u003ccode\u003eboss8.grz\u003c/code\u003e, image 16/16\"\n\t\u003e\u003c/a\u003e\u003c/figure\u003e\u003cp\u003e\n\tTo make these, I just wrote a small viewer, calling the same decompiled\n\tTH01 code: \u003ca class=\"download\" href=\"/blog/static/2020-03-07-grzview.zip?d9a8a44d\" data-kb=\"7.6\"\u003e2020-03-07-grzview.zip \u003c/a\u003e\n\tObviously, this means that it not only must to be run on a PC-98, but also\n\tdiscards the alpha information.\n\tIf any backers are \u003ci\u003ereally\u003c/i\u003e interested in having a proper converter\n\tto and from PNG, I can implement that in an upcoming push… although that\n\twould be \u003ci\u003ethe\u003c/i\u003e perfect thing for outside contributors to do.\n\u003c/p\u003e\u003cp\u003e\n\tNext up, we got some code for the PI format… oh, wait, the actual files\n\tare called \"GRP\" in TH01.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-03-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-03-07T20:45:20Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-03-03",
      "url": "https://rec98.nmlgc.net/blog/2020-03-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-03-03\"\u003e\u003ctime datetime=\"2020-03-03T12:17:54Z\"\u003e2020-03-03 12:17\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0080\"\u003eP0080\u003c/a\u003e\n\t\t\tTH01 decompilation (Graphics functions, part 5)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/cd48aa3...0252da2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e, Ember2528\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tLast part of TH01's main graphics function segment, and we've got even\n\tmore code that alternates between being boring and being slightly weird.\n\tBut at least, \u003ci\u003e\"boring\"\u003c/i\u003e also meant \u003ci\u003e\"consistent\"\u003c/i\u003e for once. And\n\tso progress continued to be as fast as expected from the last TH01 pushes,\n\tyielding 3.3% in TH01 RE%, and 1% in overall RE%, within a single day.\n\tThere even was enough time to decompile another full code segment, which\n\tbundles all the hardware initialization and cleanup calls into single\n\tfunctions to be run when starting and exiting the game. Which might be\n\tinteresting for at least one person, I guess \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tBut seriously, trying to access page 2 on a system with only page 0 and 1?\n\tHad to get out my real PC-98 to double-check that I wasn't missing\n\tanything here, since every emulator only looks at the bottom bit of the\n\tpage number. But real hardware seems to do the same, and there really is\n\tnothing special to it semantically, being equivalent to page 0. 🤷\n\u003c/p\u003e\u003cp\u003e\n\tNext up in TH01, we'll have some file format code!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-07\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-03-03T12:17:54Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-02-29",
      "url": "https://rec98.nmlgc.net/blog/2020-02-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-02-29\"\u003e\u003ctime datetime=\"2020-02-29T15:05:39Z\"\u003e2020-02-29 15:05\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0078\"\u003eP0078\u003c/a\u003e\n\t\t\tTH05 PI/RE (Stage 2 stars, Alice's puppets, Curve bullets)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f4eb7a8...9e52cb1\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0079\"\u003eP0079\u003c/a\u003e\n\t\t\tTH05 PI/RE (Mai's, Yuki's, and Shinki's 32×32 balls, Yumeko's swords)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9e52cb1...cd48aa3\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eiruleatgames, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/alice\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 3 boss.\"\u003ealice\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mai\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 4 boss (one of them).\"\u003emai\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 4 boss (one of them).\"\u003eyuki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yumeko\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 5 boss.\"\u003eyumeko\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shinki\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 6 boss.\"\u003eshinki\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/ex-alice\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Extra Stage boss.\"\u003eex-alice\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTo finish this TH05 stretch, we've got \u003cs\u003ea feature that's exclusive to TH05\n\tfor once! As the final memory management innovation in PC-98 Touhou, TH05\n\tprovides\u003c/s\u003e a single static (64 * 26)-byte array for storing up to 64\n\tentities of a custom type, specific to a stage or boss portion.\n\t(\u003cstrong\u003eEdit (2023-05-29):\u003c/strong\u003e This system actually debuted in\n\t\u003ca href=\"/blog/2023-05-29\"\u003e📝 TH04\u003c/a\u003e, where it was used for much simpler\n\tentities.)\n\u003c/p\u003e\u003cp\u003e\n\tTH05 uses this array for\n\u003c/p\u003e\u003col\u003e\n\t\u003cli\u003ethe Stage 2 star particles,\u003c/li\u003e\n\t\u003cli\u003eAlice's puppets,\u003c/li\u003e\n\t\u003cli\u003ethe tip of curve (\"jello\") bullets,\u003c/li\u003e\n\t\u003cli\u003eMai's snowballs and Yuki's fireballs,\u003c/li\u003e\n\t\u003cli\u003eYumeko's swords,\u003c/li\u003e\n\t\u003cli\u003eand Shinki's 32×32 bullets,\u003c/li\u003e\n\u003c/ol\u003e\u003cp\u003e\n\twhich makes sense, given that only one of those will be active at any\n\tgiven time.\n\u003c/p\u003e\u003cp\u003e\n\tOn the surface, they all appear to share the same 26-byte structure, with\n\tconsistently sized fields, merely using its 5 generic fields for different\n\tpurposes. Looking closer though, there actually \u003ci\u003eare\u003c/i\u003e differences in\n\tthe signedness of certain fields across the six types. uth05win chose to\n\tdeclare them as entirely separate structures, and given all the semantic\n\tdifferences (pixels vs. subpixels, regular vs. tiny master.lib sprites,\n\t…), it made sense to do the same in ReC98. It quickly turned out to be the\n\tonly solution to meet my own standards of code readability.\n\u003c/p\u003e\u003cp\u003e\n\tWhich blew this one up to two pushes once again… But now, modders can\n\ttrivially resize any of those structures without affecting the other types\n\twithin the original (64 * 26)-byte boundary, even without full position\n\tindependence. While you'd still have to reduce the type-specific\n\t\u003ci\u003enumber\u003c/i\u003e of distinct entities if you made any structure larger, you\n\tcould also have more entities with fewer structure members.\n\u003c/p\u003e\u003cp\u003e\n\tAs for the types themselves, they're full of redundancy once again – as\n\tyou might have already expected from seeing #4, #5, and #6 listed as\n\tunrelated to each other. Those could have indeed been merged into a single\n\t32×32 bullet type, supporting all the unique properties of #4\n\t(destructible, with optional revenge bullets), #5 (optional number of\n\ttwirl animation frames before they begin to move) and #6 (delay clouds).\n\tThe \u003ccode\u003e*_add()\u003c/code\u003e, \u003ccode\u003e*_update()\u003c/code\u003e, and \u003ccode\u003e*_render()\n\t\u003c/code\u003e functions of #5 and #6 could even already be completely\n\treverse-engineered from just applying the structure onto the ASM, with the\n\tones of #3 and #4 only needing one more RE push.\n\u003c/p\u003e\u003cp\u003e\n\tBut perhaps the most interesting discovery here is in the curve bullets:\n\tTH05 only renders every \u003ci\u003esecond\u003c/i\u003e one of the 17 nodes in a curve\n\tbullet, yet hit-tests every single one of them. In practice, this is an\n\tacceptable optimization though – you only start to notice jagged edges and\n\tgaps between the fragments once their speed exceeds roughly 11 pixels per\n\tsecond:\n\u003c/p\u003e\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2020-02-29-Curvebullet-Speed-11.webp?3f1b23ff\" preload=\"none\" controls loop width=\"384\" height=\"368\" data-fps=\"10\" data-frame-count=\"62\" style=\"aspect-ratio: 384 / 368\" data-lossless=\"/blog/static/video/zmbv/2020-02-29-Curvebullet-Speed-11.avi?f18bec8f\"\u003e\u003csource src=\"/blog/static/video/av1/2020-02-29-Curvebullet-Speed-11.webm?b60d6430\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2020-02-29-Curvebullet-Speed-11.webm?6a955ab2\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2020-02-29-Curvebullet-Speed-11.webm?bb502a57\" type=\"video/webm\"\u003eVideo of EX-Alice hacked to exclusively shoot curve bullets with a speed of 11 pixels a second. \u003ca href=\"/blog/static/video/zmbv/2020-02-29-Curvebullet-Speed-11.avi?f18bec8f\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\u003cp\u003e\n\tAnd that brings us to the last 20% of TH05 position independence! But\n\tfirst, we'll have more cheap and fast TH01 progress.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-03-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-02-29T15:05:39Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-02-23",
      "url": "https://rec98.nmlgc.net/blog/2020-02-23",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-02-23\"\u003e\u003ctime datetime=\"2020-02-23T16:56:09Z\"\u003e2020-02-23 16:56\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0076\"\u003eP0076\u003c/a\u003e\n\t\t\tTH03 RE (Resident structure) / decompilation (ZUN.COM)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/222fc99...9ae9754\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0077\"\u003eP0077\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 decompilation (ZUN.COM resident structure setup) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9ae9754...f4eb7a8\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/resident\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Data passed between the individual executables of each game.\"\u003eresident\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gaiji\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Custom 16×16 glyphs that can be used on the PC-98\u0026#39;s 8-color text layer.\"\u003egaiji\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWell, that took twice as long as I thought, with the two pushes containing\n\ta lot more maintenance than actual new research. Spending some time\n\timproving both field names and types in\n\t\u003ca href=\"https://www.youtube.com/channel/UChyVpooBi31k3xPbWYsoq3w\"\u003e32th System\u003c/a\u003e's\n\tTH03 resident structure finally gives us \u003ca href=\"/faq#three\"\u003eall of those\n\tstructures\u003c/a\u003e. Which means that we can now cover all the remaining\n\tdecompilable \u003ccode\u003eZUN.COM\u003c/code\u003e parts at once…\n\u003c/p\u003e\u003cp\u003e\n\tOh wait, their \u003ccode\u003emain()\u003c/code\u003e functions have stayed largely identical\n\tsince TH02? Time to clean up and separate that first, then… and combine\n\ttwo recent code generation observations into the solution to a\n\tdecompilation puzzle from 4½ years ago. Alright, time to decomp-\n\u003c/p\u003e\u003cp\u003e\n\tOh wait, we'd \u003ci\u003ekinda\u003c/i\u003e like to properly RE all the code in TH03-TH05\n\tthat deals with loading and saving .CFG files. Almost every outside\n\tcontributor wanted to grab this supposedly low-hanging fruit a lot\n\tearlier, but (of course) always just for a single game, while missing how\n\tthe format evolved.\n\u003c/p\u003e\u003cp\u003e\n\tSo, \u003ccode\u003eZUN.COM\u003c/code\u003e. For some reason, people seem to consider it\n\tparticularly important, even though it contains neither any game logic nor\n\tany code specific to PC-98 hardware… All that this decompilable part does\n\tis to initialize a game's .CFG file, allocate an empty resident structure\n\tusing master.lib functions, release it after you quit the game,\n\terror-check all that, and print some playful messages~ (OK, TH05's also\n\tdirectly fills the resident structure with all data from \u003ccode\u003e\n\tMIKO.CFG\u003c/code\u003e, which all the other games do in \u003ccode\u003eOP.EXE\u003c/code\u003e.)\n\tAt least modders can now freely change and extend all the resident\n\tstructures, as well as the .CFG files? And translators can translate those\n\tmessages that you won't see on a decently fast emulator anyway? Have fun,\n\tI guess 🤷‍\n\u003c/p\u003e\u003cp\u003e\n\tAnd you \u003ci\u003ecan\u003c/i\u003e in fact do this right now – even for TH04 and TH05,\n\twhose \u003ccode\u003eZUN.COM\u003c/code\u003e currently isn't rebuilt by ReC98. There is\n\tactually a rather involved reason for this:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eOne of the missing files is TH05's \u003ccode\u003eGJINIT.COM\u003c/code\u003e.\u003c/li\u003e\n\t\u003cli\u003eWhich contains all of TH05's gaiji characters in hardcoded 1bpp form,\n\ttogether with a bit of ASM for writing them to the PC-98's hardware gaiji\n\tRAM\u003c/li\u003e\n\t\u003cli\u003eWhich means we'd ideally first like to have a sprite compiler, for\n\t\u003ci\u003eall\u003c/i\u003e the hardcoded 1bpp sprites\u003c/li\u003e\n\t\u003cli\u003eWhich must compile to an ASM slice in the meantime, but should also\n\toutput directly to an OMF .OBJ file (for performance now), as well as to C\n\tcode (for portability later)\u003c/li\u003e\n\t\u003cli\u003eThe \u003ca href=\"https://activitypub.nmlgc.net/@rec98/statuses/01DJE866685JA15FXGR9FWP02M\"\u003e\n\tcustom build system I've been using since mid-August\u003c/a\u003e has some\n\tdeclarations for OMF .OBJ files, but it needs maybe 1 or 2 more weeks of\n\tpolish to be shipped\u003c/li\u003e\n\t\u003cli\u003eWhich I won't put in as long as the backlog contains \u003ci\u003eactual\n\tprogress\u003c/i\u003e to drive up the percentages on the front page.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t So yeah, no meaningful RE and PI progress at any of these levels. Heck,\n\t even as a modder, you can just replace the \u003ccode\u003ezun zun_res\u003c/code\u003e\n\t (TH02), \u003ccode\u003ezun -5\u003c/code\u003e (TH03), or \u003ccode\u003ezun -s\u003c/code\u003e (TH04/TH05)\n\t calls in \u003ccode\u003eGAME.BAT\u003c/code\u003e with a direct call to your modified \u003ccode\u003e\n\t *RES*.COM\u003c/code\u003e. And with the alternative being \"manually typing 0 and 1\n\t bits into a text file\", editing the sprites in TH05's\n\t \u003ccode\u003eGJINIT.COM\u003c/code\u003e is way more comfortable in a binary sprite editor\n\t anyway.\n\u003c/p\u003e\u003cp\u003e\n\tFor me though, the best part in all of this was that it finally made sense\n\tto throw out the old Borland C++ run-time assembly slices 🗑 This giant\n\twaste of time \u003ca href=\"https://github.com/nmlgc/ReC98/commit/c2a8c22\"\u003e\n\tbecame obvious 5 years ago\u003c/a\u003e, but any ASM dump of a \u003ccode\u003e.COM\u003c/code\u003e\n\tfile would have needed rather ugly workarounds without those slices. Now\n\tthat all .COM binaries that were originally written in C \u003ci\u003eare\u003c/i\u003e\n\tcompiled from C, we can all enjoy slightly faster grepping over the entire\n\trepository, which now has 229 fewer files. Productivity will skyrocket!\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\u003cp\u003e\n\tNext up: Three weeks of almost full-time ReC98 work! Two more PI-focused\n\tpushes to finish this TH05 stretch first, before switching priorities to\n\tTH01 again.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-02-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-02-23T16:56:09Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-02-16",
      "url": "https://rec98.nmlgc.net/blog/2020-02-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-02-16\"\u003e\u003ctime datetime=\"2020-02-16T20:51:42Z\"\u003e2020-02-16 20:51\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0072\"\u003eP0072\u003c/a\u003e\n\t\t\tTH04/TH05 PI (Bullet structure)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/4bb04ab...cea3ea6\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0073\"\u003eP0073\u003c/a\u003e\n\t\t\tTH04/TH05 RE (32×32 + monochrome 16×16 sprite rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/cea3ea6...5286417\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0074\"\u003eP0074\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Bullet sprites)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/5286417...1807906\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0075\"\u003eP0075\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Bullet group types, spawn types, and templates)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1807906...222fc99\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e, Myles\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bullet\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by enemies.\"\u003ebullet\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tLong time no see! And this is exactly why I've been procrastinating\n\tbullets while there was still meaningful progress to be had in other parts\n\tof TH04 and TH05: There was bound to be quite some complexity in this most\n\tcentral piece of game logic, and so I couldn't possibly get to a\n\tsatisfying understanding in just one push.\n\u003c/p\u003e\u003cp\u003e\n\tOr in two, because their rendering involves another bunch of\n\tmicro-optimized functions adapted from master.lib.\n\u003c/p\u003e\u003cp\u003e\n\tOr in three, because we'd like to actually name all the bullet sprites,\n\tsince there are a number of sprite ID-related conditional branches. And\n\tso, I was refining things I supposedly RE'd in the the commits from the\n\tfirst push until the very end of the fourth.\n\u003c/p\u003e\u003cp\u003e\n\tWhen we talk about \"bullets\" in TH04 and TH05, we mean just two things:\n\tthe white 8×8 pellets, with a cap of 240 in TH04 and 180 in TH05, and any\n\t16×16 sprites from \u003ccode\u003eMIKO16.BFT\u003c/code\u003e, with a cap of 200 in TH04 and\n\t220 in TH05. These are by far the most common types of… err, \"things the\n\tplayer can collide with\", and so ZUN provides a whole bunch of pre-made\n\tmotion, animation, and \u003ca href=\"https://sparen.github.io/ph3tutorials/ddsga3.html\"\u003e\n\tn-way spread / ring / stack\u003c/a\u003e group options for those, which can be\n\tselected by simply setting a few fields in the bullet template. All the\n\tother \"non-bullets\" have to be fired and controlled individually.\n\u003c/p\u003e\u003cp\u003e\n\tWhich is nothing new, since uth05win covered this part pretty accurately –\n\tI don't think \u003ci\u003eanyone\u003c/i\u003e could just make up these structure member\n\toverloads. The interesting insights here all come from applying this\n\tresearch to TH04, and figuring out its differences compared to TH05. The\n\tmost notable one there is in the default groups: TH05 allows you to add\n\ta \u003ca href=\"https://sparen.github.io/ph3tutorials/ddsga3.html#sub5\"\u003estack\u003c/a\u003e\n\tto any single bullet, n-way spread or ring, but TH04 only lets you create\n\tstacks separately from n-way spreads and rings, and thus gets by with\n\tfewer fields in its bullet template structure. On the other hand, TH04 has\n\ta separate \"n-way spread with random angles, yet still aimed at the\n\tplayer\" group? Which \u003ci\u003eseems\u003c/i\u003e to be unused, at least as far as\n\tmidbosses and bosses are concerned; can't say anything about stage enemies\n\tyet.\n\u003c/p\u003e\u003cp\u003e\n\tIn fact, TH05's larger bullet template structure illustrates that these\n\tdistinct group types actually are a rather redundant piece of\n\tover-engineering. You can perfectly indicate any permutation of the basic\n\tgroups through just the stack bullet count (1 = no stack), spread bullet\n\tcount (1 = no spread), and spread delta angle (0 = ring instead of\n\tspread). Add a 4-flag bitfield to cover the rest (aim to player, randomize\n\tangle, randomize speed, force single bullet regardless of difficulty or\n\trank), and the result would be less redundant \u003ci\u003eand\u003c/i\u003e even slightly\n\tmore capable.\n\u003c/p\u003e\u003cp\u003e\n\tEven those 4 pushes didn't quite finish all of the bullet-related types,\n\tstopping just shy of the most trivial and consistent enum that defines\n\tspecial movement. This also left us in a\n\t\u003ca href=\"/blog/2020-01-29\"\u003e📝 TH03-like situation\u003c/a\u003e, in which we're still\n\ta bit away from actually converting all this research into actual RE%. Oh\n\twell, at least this got us way past 50% in overall position independence.\n\tOn to the second half! 🎉\n\u003c/p\u003e\u003cp\u003e\n\tFor the next push though, we'll first have a quick detour to the remaining\n\tC code of all the \u003ccode\u003eZUN.COM\u003c/code\u003e binaries. Now that the\n\t\u003ca href=\"/blog/2020-01-03\"\u003e📝 TH04 and TH05 resident structures\u003c/a\u003e no\n\tlonger block those, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e has requested TH05's\n\t\u003ccode\u003eRES_KSO.COM\u003c/code\u003e to be covered in one of his outstanding pushes.\n\tAnd since \u003ca href=\"https://www.youtube.com/channel/UChyVpooBi31k3xPbWYsoq3w\"\u003e32th System\u003c/a\u003e\n\trecently RE'd TH03's resident structure, it makes sense to also review and\n\tmerge that, before decompiling all three remaining \u003ccode\u003eRES_*.COM\u003c/code\u003e\n\tbinaries in hopefully a single push. It might even get done faster than\n\tthat, in which case I'll then review and merge some more of\n\t\u003ca href=\"https://github.com/wintiger0222/ReC98\"\u003eWindowsTiger\u003c/a\u003e's\n\tresearch.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-02-16T20:51:42Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-01-29",
      "url": "https://rec98.nmlgc.net/blog/2020-01-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-01-29\"\u003e\u003ctime datetime=\"2020-01-29T08:17:30Z\"\u003e2020-01-29 08:17\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0071\"\u003eP0071\u003c/a\u003e\n\t\t\tTH03 RE (Player structure)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/327021b...4bb04ab\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://www.twitch.tv/KirbyComment\"\u003eKirbyComment\u003c/a\u003e, \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tTurns out that covering TH03's 128-byte player structure \u003ci\u003ewas\u003c/i\u003e way\n\tmore insightful than expected! And while it doesn't include \u003ci\u003eevery\u003c/i\u003e\n\tbit of per-player data, we still got to know quite a bit about the game\n\tfrom just trying to name its members:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e50 frames of invincibility when starting a new round\u003c/li\u003e\n\t\u003cli\u003e110 frames of invincibility when getting hit\u003c/li\u003e\n\t\u003cli\u003e64 frames of knockback when getting hit\u003c/li\u003e\n\t\u003cli\u003e128 frames before a charged up gauge/boss attack is fired\n\tautomatically\u003c/li\u003e\n\t\u003cli\u003eThe damage a player will take from the next hit starts out at ½ heart\n\tat the beginning of each round, and increases by another ½ heart every\n\t1024 frames, capped at a maximum of 3 hearts. This guarantees that a\n\tplayer will always survive at least two hits.\u003c/li\u003e\n\t\u003cli\u003eIn Story Mode, hit damage is biased in favor of the player for the\n\tfirst 6 stages. The CPU will always take an additional 1½ hearts of damage\n\tin stages 1 and 2, 1 heart in stages 3 and 4, and ½ heart in stages 5 and\n\t6, plus the above frame-based and capped damage amount. So while it's\n\ttherefore possible to cause 4½ hearts of damage in Stages 1 and 2 if the\n\tfirst hit is somehow delayed for at least 5120 frames, you'd still win\n\tfaster if the CPU gets hit as soon as possible.\u003c/li\u003e\n\t\u003cli\u003eCPU players will charge up a gauge/boss attack as soon as their gauge\n\thas reached a certain level. These levels are now proved to be random; at\n\tthe start of every round, the game generates a sequence of 64 gauge level\n\tpositions (from 1 to 4), separately for each player. If a round were to\n\tlast long enough for a CPU player to fire all 64 of those predetermined\n\tattacks, you'd observe that sequence repeating.\u003cul\u003e\n\t\t\u003cli\u003eYes, that means that in theory, these levels can be\n\t\tRNG-manipulated. More details on that once we got this game's resident\n\t\tstructure, where the seed is stored.\u003c/li\u003e\n\t\u003c/ul\u003e\u003c/li\u003e\n\t\u003cli\u003eCPU players follow two main strategies: trying to not get hit, and…\n\tnot quite doing that once they've survived for a certain safety threshold\n\tof frames. For the first 2000 frames of a round, this safety frame counter\n\tis reset to 0 every 64 frames, leading the CPU to switch quickly between\n\tthe two strategies in the first few Story Mode stages on lower\n\tdifficulties, where this safety threshold is less than 64. The calculation\n\tof the actual value is a bit more complex; more on that also once we got\n\tthis game's resident structure.\u003c/li\u003e\n\t\u003cli\u003eSection 13 of \u003ca href=\"https://en.touhouwiki.net/wiki/Phantasmagoria_of_Dim.Dream/Translation/Manual\"\u003e\n\t\u003ccode\u003e夢時空.TXT\u003c/code\u003e\u003c/a\u003e states that Boss Attacks are only counted\n\ttowards the Clear Bonus if they were caused by reaching a certain number\n\tof spell points. This is incorrect; manually charged Level 4 Boss Attacks\n\tare counted as well.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tThe next TH03 pushes can now cover all the functions that reference this\n\tstructure in one way or another, and actually commit all this research and\n\ttranslate it into some RE%. Since the non-TH05 priorities have become a\n\tbit unclear after the last 50\u0026nbsp;€ RE contribution though (as of this\n\twriting, it's still 10\u0026nbsp;€ to decide on what game to cover in two RE\n\tpushes!), I'll be returning to TH05 until that's decided.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-02-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-20\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-01-29T08:17:30Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-01-20",
      "url": "https://rec98.nmlgc.net/blog/2020-01-20",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-01-20\"\u003e\u003ctime datetime=\"2020-01-20T21:51:25Z\"\u003e2020-01-20 21:51\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0070\"\u003eP0070\u003c/a\u003e\n\t\t\tTH03 RE (Score and combo variables)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/a931758...327021b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://www.twitch.tv/KirbyComment\"\u003eKirbyComment\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAs noted in \u003ca href=\"/blog/2019-12-05\"\u003e📝 P0061\u003c/a\u003e, TH03 gameplay RE is\n\tindeed going to progress \u003ci\u003every\u003c/i\u003e slowly in the beginning. A lot of the\n\tinitial progress won't even be reflected in the RE% – there are just so\n\tmany features in this game that are intertwined into each other, and I\n\tonly consider functions to be \"reverse-engineered\" once we understand\n\t\u003ci\u003eevery\u003c/i\u003e involved piece of code and data, and labeled every absolute\n\tmemory reference in it. (Yes, that means that the percentages on the front\n\tpage are actually underselling ReC98's progress quite a bit, and reflect a\n\tpretty low bound of our actual understanding of the games.)\n\u003c/p\u003e\u003cp\u003e\n\tSo, when I get asked to look directly at gameplay code \u003ci\u003eright now\u003c/i\u003e,\n\tit's quite the struggle to find a place that can be covered within a push\n\tor two \u003ci\u003eand\u003c/i\u003e that would immediately benefit\n\t\u003ca href=\"https://en.touhouwiki.net/wiki/User:KirbyComment/Phantasmagoria_of_Dimensional_Dream_Info\"\u003e\n\tscoreplayers\u003c/a\u003e. The basics of score and combo handling themselves\n\tmanaged to fit in pretty well, though:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eJust like TH04 and TH05, TH03 stores the current score as 8\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\"\u003ebinary-coded\n\tdecimal\u003c/a\u003e digits. Since the last constant 0 is not included, the maximum\n\tscore displayable without glitches therefore is 999,999,990 points, but\n\tthe game will happily store up to 24,699,999,990 points before the score\n\twraps back to 0.\u003c/li\u003e\n\t\u003cli\u003eThere are (surprisingly?) only 6 places where the game actually\n\tadds points to the score. Not quite sure about all of them yet, but they\n\t(of course) include ending a combo, killing enemies, and the bonus at the\n\tend of a round.\u003c/li\u003e\n\t\u003cli\u003eCombos can be continued for 80 frames after a 2-hit. The hit counter\n\tcan only be increased in the first 48, and effectively resets to 0 for the\n\tlast 32, when the Spell Point value starts blinking.\u003c/li\u003e\n\t\u003cli\u003eTH03 can track a total of 16 independent \"hit combo sources\" per\n\tplayer, simultaneously. These are \u003ci\u003enot\u003c/i\u003e related to the number of\n\tactual explosions; rather, each explosion is assigned to one of the 16\n\tslots when it spawns, and all consecutive explosions spawned from that one\n\twill then add to the hit combo in that slot. The hit number displayed in\n\tthe top left is simply the largest one among all these.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tOh well, at least we still got a bit of PI% out of this one. From this\n\tpoint though, the next push (or two) should be enough to cover the big\n\t128-byte player structure – which by itself might not be immediately\n\tinteresting to scoreplayers, but surely is quite a blocker for everything\n\telse.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-01-20T21:51:25Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-01-14",
      "url": "https://rec98.nmlgc.net/blog/2020-01-14",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-01-14\"\u003e\u003ctime datetime=\"2020-01-14T21:15:19Z\"\u003e2020-01-14 21:15\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0067\"\u003eP0067\u003c/a\u003e\n\t\t\tTH01 decompilation (Graphics functions, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e55a48b...ebb30ce\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0068\"\u003eP0068\u003c/a\u003e\n\t\t\tTH01 decompilation (Graphics functions, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ebb30ce...2ac00d4\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0069\"\u003eP0069\u003c/a\u003e\n\t\t\tTH01 decompilation (Graphics functions, part 4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e0d0dcd...0f18dbc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e, Yanga, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/glitch\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Bugs in the original code that affect visuals or gameplay in minor ways.\"\u003eglitch\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mima-th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 地獄/Jigoku route.\"\u003emima-th01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yuugenmagan\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH01\u0026#39;s Stage 10 boss, on the 魔界/Makai route.\"\u003eyuugenmagan\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNow \u003ci\u003ethat's\u003c/i\u003e more like the speed I was expecting! After a few more\n\tunused functions for palette fading and rectangle blitting, we've reached\n\tthe big line drawing functions. And the biggest one among \u003ci\u003ethem\u003c/i\u003e,\n\tdrawing a straight line at any angle between two points using\n\t\u003ca href=\"https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm\"\u003e\n\tBresenham's algorithm\u003c/a\u003e, actually happens to be the single longest\n\tfunction present in more than one binary in all of PC-98 Touhou, and #23\n\ton the list of individual longest functions.\n\u003c/p\u003e\u003cp\u003e\n\tAnd it \u003ci\u003etechnically\u003c/i\u003e has a ZUN bug! If you pass a point outside the\n\t(0, 0) - (639, 399) screen range, the function will calculate a new point\n\tat the edge of the screen, so that the resulting line will retain the\n\tangle intended by the points given. Except that it does so by calculating\n\tthe line slope using an integer division rather than a floating-point one\n\t\u003cimg src=\"/static/emoji-zunpet.png?f4e41431\" alt=\":zunpet:\" width=\"24\" height=\"24\" \u003e Doesn't seem like it actually causes any weirdly\n\tskewed lines to be drawn in-game, though; that case is only hit in the\n\tMima boss fight, which draws a few lines with a bottom coordinate of\n\t400 rather than the maximum of 399. It \u003ci\u003emight\u003c/i\u003e also cause the wrong\n\tbackground pixels to be restored during parts of the YuugenMagan fight,\n\tleading to flickering sprites, but seriously, that's an issue everywhere\n\tyou look in this game.\n\u003c/p\u003e\u003cp\u003e\n\tTogether with the rendering-text-to-VRAM function we've mostly already\n\tknown from TH02, this pushed the total RE percentage well over 20%, and\n\talmost doubled the TH01 RE percentage, all within three pushes. And\n\tcomparatively, it went \u003ci\u003ereally\u003c/i\u003e smoothly, to the point (ha) where I\n\teven had enough time left to also include the single-point functions that\n\tcome next in that code segment. Since about half of the remaining\n\tfunctions in \u003ccode\u003eOP.EXE\u003c/code\u003e are present in more than just itself,\n\tI'll be able to at least keep up this speed until \u003ccode\u003eOP.EXE\u003c/code\u003e hits\n\tthe 70% RE mark. That is, as long as the backers' priorities continue to\n\tbe generic RE or \"giving some love to TH01\"… we don't have a precedent for\n\tTH01's actual game code yet.\n\u003c/p\u003e\u003cp\u003e\n\tAnd that's all the TH01 progress funded for January! Next up, we actually\n\t\u003ci\u003edo\u003c/i\u003e have a focus on TH03's game and scoring mechanics… or at least\n\tthe foundation for that.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-20\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-01-14T21:15:19Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-01-05",
      "url": "https://rec98.nmlgc.net/blog/2020-01-05",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-01-05\"\u003e\u003ctime datetime=\"2020-01-05T20:04:19Z\"\u003e2020-01-05 20:04\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0066\"\u003eP0066\u003c/a\u003e\n\t\t\tTH01 RE (Palettes) / decompilation (Graphics functions, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/042b780...e55a48b\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003eYanga, \u003ca class=\"customer\" href=\"http://realitydreamers.de/\"\u003eSplashman\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/palette\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Manipulation of the PC-98\u0026#39;s 16-color palette for graphics.\"\u003epalette\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, the thing that made me so excited about TH01 were all those bulky C\n\treimplementations of master.lib functions. Identical copies in all three\n\texecutables, trivial to figure out and decompile, removing tons of\n\tinstructions, and providing a foundation for large parts of the game\n\tlater. The first set of functions near the end of that shared code segment\n\tdeals with color palette handling, and master.lib's resident palette\n\tstructure in particular. (No relation to \u003ca href=\"/faq#three\"\u003ethe game's\n\tresident structure.\u003c/a\u003e) Which directly starts us out with pretty much\n\t\u003ci\u003eall\u003c/i\u003e the decompilation difficulties imaginable:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eiteration over internal DOS structures via segment pointers – Turbo\n\tC++ doesn't support a lot of arithmetic on those, requiring tons of casts\n\tto make it work\u003c/li\u003e\n\t\u003cli\u003ecalls to a \u003ccode\u003efar\u003c/code\u003e function near the beginning of a segment\n\tfrom a function near the end of a segment – these are undecompilable until\n\twe've decompiled both functions (and thus, the majority of the segment),\n\tand need to be spelled out in ASM for the time being. And if the caller\n\t\u003ci\u003ethen\u003c/i\u003e stores some of the involved variables in registers, there's no\n\tway around the ugliest of workarounds, \u003ci\u003espelling out opcode bytes\u003c/i\u003e…\n\t\u003c/li\u003e\n\t\u003cli\u003esurprising color format inconsistencies – apparently, GRB (rather than\n\tRGB) is some sort of wider standard in PC-98 inter-process communication,\n\tbecause it matches the order of the hardware's palette register ports?\n\t(\u003cspan style=\"color: green;\"\u003e\u003ccode\u003e0AAh\u003c/code\u003e = green\u003c/span\u003e,\n\t\u003cspan style=\"color: red;\"\u003e\u003ccode\u003e0ACh\u003c/code\u003e = red\u003c/span\u003e,\n\t\u003cspan style=\"color: blue;\"\u003e\u003ccode\u003e0AEh\u003c/code\u003e = blue\u003c/span\u003e)? Yet the\n\tgame's actual palette still uses RGB…\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tAnd as it turns out, the game doesn't even use the resident palette\n\tfeature. Which adds yet another set of functions to the, uh, learning\n\texperience that ZUN must have chosen this game to be. I wouldn't be\n\tsurprised if we manage to uncover actual scrapped beta game content later\n\ton, among all the unused code that's bound to still be in there.\n\u003c/p\u003e\u003cp\u003e\n\tAt least decompilation should get easier for the next few TH01 pushes now…\n\tright?\n\u003c/p\u003e\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2020-01-03\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-01-05T20:04:19Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2020-01-03",
      "url": "https://rec98.nmlgc.net/blog/2020-01-03",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2020-01-03\"\u003e\u003ctime datetime=\"2020-01-03T20:45:46Z\"\u003e2020-01-03 20:45\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0065\"\u003eP0065\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Resident structures)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9faa29a...042b780\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/resident\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Data passed between the individual executables of each game.\"\u003eresident\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tA~nd \u003ca href=\"/faq#three\"\u003eresident structures\u003c/a\u003e ended up being exactly\n\tthe right thing to start off the new year with.\n\t\u003ca href=\"https://twitter.com/WindowsTiger\"\u003eWindowsTiger\u003c/a\u003e and\n\t\u003ca href=\"https://twitter.com/spaztron64\"\u003espaztron64\u003c/a\u003e have already been\n\tpushing for them with their own reverse-engineering, and together with my\n\town recent \u003ccode\u003eGENSOU.SCR\u003c/code\u003e RE work, we've clarified just enough\n\tcontext around the harder-to-explain values to make both TH04's and TH05's\n\tstructures fit nicely into the typical time frame of a single push.\n\u003c/p\u003e\u003cp\u003e\n\tWith all the apparently obvious and seemingly just duplicated values, it\n\thas always been easy to do a superficial job for most of the structure,\n\tthen lose motivation for the last few unknown fields. Pretty glad to got\n\tthis finally covered; I've heard that people are going to write trainer\n\ttools now?\n\u003c/p\u003e\u003cp\u003e\n\tAlso, where better to slot in a push that, in terms of figures, seems to\n\tdeliver 0% RE and only miniscule PI progress, than at the end of\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e's 5-push order that already had multiple pushes\n\tyielding above-average progress? \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e As usual,\n\twe'll be reaping the rewards of this work in the next few TH04/TH05\n\tpushes…\n\u003c/p\u003e\u003cp\u003e\n\t…whenever they get funded, that is, as for January, the backers have\n\tshifted the priorities towards TH01 and TH03. TH01 especially is something\n\tI'm quite excited about, as we're finally going to see just how fast this\n\tbloated game is really going to progress. Are you excited?\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2020-01-03T20:45:46Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-12-29",
      "url": "https://rec98.nmlgc.net/blog/2019-12-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-12-29\"\u003e\u003ctime datetime=\"2019-12-29T20:23:06Z\"\u003e2019-12-29 20:23\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0064\"\u003eP0064\u003c/a\u003e\n\t\t\tTH04/TH05 PI (Completing OP.EXE)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/80cec5b...9faa29a\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/menu\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Any window offering multiple options to be selected or configured.\"\u003emenu\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t🎉 TH04's and TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e are now fully\n\tposition-independent! 🎉\n\u003c/p\u003e\u003ch5\u003eWhat does this mean?\u003c/h5\u003e\u003cp\u003e\n\tYou can now add any data or code to the main menus of the two games, by\n\tsimply editing the ReC98 source, writing your mod in ASM or C/C++, and\n\trecompiling the code. Since all absolute memory addresses have now been\n\tconverted to labels, this will work without causing any instability. See\n\tthe \u003ca href=\"/faq#pi-what\"\u003eposition independence section in the FAQ\u003c/a\u003e\n\tfor a more thorough explanation about why this was a problem.\n\u003c/p\u003e\u003ch5\u003eWhat does this not mean?\u003c/h5\u003e\u003cp\u003e\n\tThe original ZUN code hasn't been completely reverse-engineered yet, let\n\talone decompiled. Pretty much all of that is still ASM, which might make\n\tmodding a bit inconvenient right now.\n\u003c/p\u003e\u003cp\u003e\n\tSince this push was otherwise pretty unremarkable, I made a video\n\tdemonstrating a few basic things you can do with this:\n\t\u003cscript\u003eexternalRegister('2019-12-29', 'vid', 'https://youtube.com/embed/iqPxiaaHIhA');\u003c/script\u003e\n\u003c/p\u003e\u003cfigure\u003e\n\t\u003ciframe id=\"2019-12-29-vid\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tNow, what to do for the last outstanding \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e push?\n\tBullets, or resident structures? \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2020-01-03\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-12-29T20:23:06Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-12-28",
      "url": "https://rec98.nmlgc.net/blog/2019-12-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-12-28\"\u003e\u003ctime datetime=\"2019-12-28T11:37:29Z\"\u003e2019-12-28 11:37\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0063\"\u003eP0063\u003c/a\u003e\n\t\t\tTH05 RE (GENSOU.SCR, part 1/3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/034ae4b...8dbb450\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t\u003ci\u003eAlmost!\u003c/i\u003e\n\u003c/p\u003e\u003cp\u003e\n\tJust like most of the time, it was more sensible to cover\n\t\u003ccode\u003eGENSOU.SCR\u003c/code\u003e, the last structure missing in TH05's \u003ccode\u003e\n\tOP.EXE\u003c/code\u003e,\n\teverywhere it's used, rather than just rushing out \u003ccode\u003eOP.EXE\u003c/code\u003e\n\tposition independence. I did have to look into all of the functions to\n\tfully RE it after all, and to find out whether the unused fields actually\n\t\u003ci\u003eare\u003c/i\u003e unused. The only thing that kept this push from yielding even\n\tmore above-average progress was the sheer inconsistency in how the games\n\timplemented the operations on this PC-98 equivalent of \u003ccode\u003escore*.dat\u003c/code\u003e:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\u003ccode\u003eOP.EXE\u003c/code\u003e declares two structure instances, for simultaneous\n\taccess to both Reimu and Marisa scores. TH05 with its 4 playable\n\tcharacters instead uses a single one, and overwrites it successively for\n\teach character when drawing the high score menu – meaning, you'd only see\n\tYuuka's scores when looking at the structure inside the rendered high\n\tscore menu. However, it still declares the TH04 \"Marisa\" structure as a\n\tleftover… \u003ci\u003eand also decodes it and verifies its checksum\u003c/i\u003e, despite\n\tnothing being ever loaded into it\u003c/li\u003e\n\t\u003cli\u003e\u003ccode\u003eMAIN.EXE\u003c/code\u003e uses a separate ASM implementation of the decoding\n\tand encoding functions \u003cimg src=\"/static/emoji-godzun.png?dc85c97a\" alt=\":godzun:\" width=\"24\" height=\"24\" \u003e\u003c/li\u003e\n\t\u003cli\u003eTH05's \u003ccode\u003eMAIN.EXE\u003c/code\u003e also reimplements the basic loading\n\tfunctions\n\tin ASM – \u003ci\u003ewithout\u003c/i\u003e the code to regenerate \u003ccode\u003eGENSOU.SCR\u003c/code\u003e with\n\tdefault data if the file is missing or corrupted. That actually makes\n\tsense, since any regeneration is already done in \u003ccode\u003eOP.EXE\u003c/code\u003e, which\n\talways has to load that file anyway to check how much has been cleared\n\t\u003c/li\u003e\n\t\u003cli\u003eHowever, there \u003ci\u003eis\u003c/i\u003e a regeneration function in TH05's\n\t\u003ccode\u003eMAINE.EXE\u003c/code\u003e… which actually generates \u003ci\u003edifferent\u003c/i\u003e default\n\tdata: \u003ccode\u003eOP.EXE\u003c/code\u003e consistently sets Extra Stage records to Stage 1,\n\twhile \u003ccode\u003eMAINE.EXE\u003c/code\u003e uses the same place-based stage numbering that\n\tboth versions use for the regular ranks\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t\u003ci\u003eTechnically\u003c/i\u003e though, TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e \u003ci\u003eis\u003c/i\u003e\n\tposition-independent now, and the rest are (\u003ci\u003eshould\u003c/i\u003e be?\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e) merely false positives. However, TH04's is\n\tstill missing another structure, in addition to \u003ci\u003eits\u003c/i\u003e false\n\tpositives. So, let's wait with the big announcement until the next push…\n\twhich will also come with a demo video of what will be possible then.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-22\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-12-28T11:37:29Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-12-22",
      "url": "https://rec98.nmlgc.net/blog/2019-12-22",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-12-22\"\u003e\u003ctime datetime=\"2019-12-22T15:09:53Z\"\u003e2019-12-22 15:09\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0062\"\u003eP0062\u003c/a\u003e\n\t\t\tTH05 decompilation (Reimu's shot functions) / PI\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1d6fbb8...f275e04\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBig gains, as expected, but not much to say about this one. With TH05 Reimu\n\tbeing \u003ci\u003eway\u003c/i\u003e too easy to decompile after\n\t\u003ca href=\"/blog/2019-10-14\"\u003e📝 the shot control groundwork done in October\u003c/a\u003e,\n\tthere was enough time to give the comprehensive PI false-positive\n\ttreatment to two other sets of functions present in TH04's and TH05's\n\t\u003ccode\u003eOP.EXE\u003c/code\u003e. One of them, master.lib's \u003ccode\u003esuper_*()\u003c/code\u003e\n\tfunctions, was used \u003ci\u003ea lot\u003c/i\u003e in TH02, more than in any other game… I\n\twonder how much more that game will progress without even focusing on it\n\tin particular.\n\u003c/p\u003e\u003cp\u003e\n\tAlright then! 100% PI for TH04's and TH05's \u003ccode\u003eOP.EXE\u003c/code\u003e upcoming…\n\t(\u003cb\u003e\u003ci\u003eEdit:\u003c/i\u003e\u003c/b\u003e Already got funding to cover this!)\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-12-22T15:09:53Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-12-18",
      "url": "https://rec98.nmlgc.net/blog/2019-12-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-12-18\"\u003e\u003ctime datetime=\"2019-12-18T15:00:00Z\"\u003e2019-12-18\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tDid \u003ca href=\"https://twitter.com/WindowsTiger\"\u003eWindowsTiger\u003c/a\u003e just cover\n\t2% over all games \u003ca href=\"https://github.com/wintiger0222/ReC98\"\u003eon his\n\town\u003c/a\u003e? While not all of that passed my review, +1.59% RE and +1.66% PI\n\tover all 5 games is still pretty noteworthy, and comfortably pushes TH05\n\tover the 25% mark in RE, and the 60% mark in PI.\n\u003c/p\u003e\u003cp\u003e\n\t\u003cb\u003e\u003ci\u003eHowever.\u003c/i\u003e\u003c/b\u003e\n\u003c/p\u003e\u003cp\u003e\n\tWhile I definitely do appreciate such contributions, reviewing and\n\tadapting these to my current code organization standards also takes more\n\ttime than I'd like it to take. And taken to this level, it \u003ci\u003edoes\u003c/i\u003e\n\tkind of undermine this crowdfunding project, causing both a \u003ci\u003eliteral\u003c/i\u003e\n\tdenial of service and exactly the stress that this crowdfunding was\n\tdesigned to avoid. Most of the time, I can't merge all of that as-is\n\twithout knowingly creating annoyances down the line. But I don't want to\n\tjust ignore it either, or reject every non-perfect commit…\u003cbr\u003e\n\tThat's also why I let it slide this time, due to some of the RE work in\n\tthere being genuinely amazing. In the future though, be aware that your\n\tchance of having your work merged diminishes the further you move ahead of\n\tmy current \u003ccode\u003emaster\u003c/code\u003e branch. In extreme cases like this one, I'll\n\tthen just be waiting until enough generic reverse-engineering pushes have\n\taccrued, and treat the merge as regular work.\n\u003c/p\u003e\u003cp\u003e\n\tBut now, time to continue with the regular programming… I \u003ci\u003eam\u003c/i\u003e kind\n\tof exhausted from all of this, so no bullets for the next two\n\t\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e pushes, still… Good thing there's still plenty of\n\tsimpler things with big percentage gains to be done:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eWindowsTiger mostly focused on \u003ccode\u003eOP.EXE\u003c/code\u003e which I tended to\n\tneglect, as the big \u003ccode\u003eMAIN\u003c/code\u003e executables seemed to be more\n\tinteresting to my backers. (It's not like anyone ever \u003ci\u003erequested\u003c/i\u003e\n\t\u003ccode\u003eOP\u003c/code\u003e to be done either – like, who even cares about boring menu\n\tsource code, right?) Good that I therefore sort of left it as low-hanging\n\tfruit to be grabbed by outside contributors – because now, TH04's and\n\tTH05's \u003ccode\u003eOP.EXE\u003c/code\u003e are close to 100% position-independence. The\n\t\u003ccode\u003eGENSOU.SCR\u003c/code\u003e format is pretty much the only thing missing there\n\tright now, so let's finally go all the way there, I'd say.\u003c/li\u003e\n\t\u003cli\u003eAnd in TH05, there's still Reimu's shot type functions left to be\n\tdecompiled.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-22\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-12-05\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-12-18T16:00:00+01:00"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-12-05",
      "url": "https://rec98.nmlgc.net/blog/2019-12-05",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-12-05\"\u003e\u003ctime datetime=\"2019-12-05T21:18:16Z\"\u003e2019-12-05 21:18\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0061\"\u003eP0061\u003c/a\u003e\n\t\t\tTH03 RE (Character data, part 1 + Player shots, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/96684f4...5f4f5d8\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\t… nope, with a game whose \u003ccode\u003eMAIN.EXE\u003c/code\u003e is still just 5%\n\treverse-engineered \u003ci\u003eand\u003c/i\u003e which naturally makes heavy use of\n\tstructures, there's still a lot more PI groundwork to be done before RE\n\tprogress can speed up to the levels that we've now reached with TH05. The\n\tgood news is that this game is (now) way easier to understand: In contrast\n\tto TH04 and TH05, where we needed to work towards player shots over a\n\ttwo-digit number of pushes, TH03 only needed two for SPRITE16, and a half\n\tone for the playfield shaking mechanism. After that, I could even already\n\tdecompile the per-frame shot update and render functions, thanks to TH03's\n\thigh number of code segments. Now, even the big 128-byte player structure\n\tdoesn't seem all too far off.\n\u003c/p\u003e\u003cp\u003e\n\tThen again, as TH03 shares no code with any other game, this actually was\n\ta completely average PI push. For the remaining three, we'll return to\n\tTH04 and TH05 though, which should more than make up for the slight drop\n\tin RE speed after this one.\n\u003c/p\u003e\u003cp\u003e\n\tIn other news, we've now also reached peak C++, with the introduction of\n\t\u003ccode\u003etemplate\u003c/code\u003es! TH03 stores movement speeds in a 4.4 fixed-point\n\tformat, which is an 8-bit spin on the usual 16-bit, 12.4 fixed-point\n\tformat.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-29\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-12-05T21:18:16Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-11-29",
      "url": "https://rec98.nmlgc.net/blog/2019-11-29",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-11-29\"\u003e\u003ctime datetime=\"2019-11-29T00:15:36Z\"\u003e2019-11-29 00:15\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0060\"\u003eP0060\u003c/a\u003e\n\t\t\tTH03 decompilation (The stolen sprite driver, part 2) / PI \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/29385dd...73f5ae7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, where to start? Well, TH04 bullets are hard, so let's\n\t\u003cs\u003eprocrastinate\u003c/s\u003e start with TH03 instead \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e\n\tThe \u003ca href=\"/blog/2019-11-06\"\u003e📝 sprite display functions\u003c/a\u003e are the\n\tobvious blocker for any structure describing a sprite, and therefore most\n\tmeaningful PI gains in that game… and I actually did manage to fit a\n\tdecompilation of those three functions into exactly the amount of time\n\tthat the \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e community votes alloted to TH03\n\treverse-engineering!\n\u003c/p\u003e\u003cp\u003e\n\tAnd a pretty amazing one at that. The original code was so obviously\n\twritten in ASM and was just barely decompilable by exclusively using\n\tregister pseudovariables and a bit of \u003ccode\u003egoto\u003c/code\u003e, but I was able to\n\tabstract most of that away, not least thanks to a few helpful optimization\n\tproperties of Turbo C++… seriously, I can't stop marveling at this ancient\n\tcompiler. The end result is both readable, clear, and dare I say\n\t\u003ci\u003eportable\u003c/i\u003e?! To anyone interested in porting TH03,\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/th03/sprite16.cpp\"\u003e\n\ttake a look. How painful would it be to port \u003ci\u003ethat\u003c/i\u003e away from 16-bit\n\tx86\u003c/a\u003e?\n\u003c/p\u003e\u003cp\u003e\n\tHowever, this push is also a typical example that the RE/PI priorities can\n\tonly control what I \u003ci\u003elook\u003c/i\u003e at, and the outcome can actually differ\n\tgreatly. Even though the priorities were 65% RE and 35% PI, the progress\n\toutcome was +0.13% RE and +1.35% PI. But hey, we've got one more push with\n\ta focus on TH03 PI, so maybe \u003ci\u003ethat \u003c/i\u003e one will include more RE than\n\tPI, and then everything will end up just as ordered?\n\t\u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-12-05\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-18\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-11-29T00:15:36Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-11-18",
      "url": "https://rec98.nmlgc.net/blog/2019-11-18",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-11-18\"\u003e\u003ctime datetime=\"2019-11-18T21:28:01Z\"\u003e2019-11-18 21:28\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0059\"\u003eP0059\u003c/a\u003e\n\t\t\tTH04/TH05 PI (Motion structures + EGC calls + vector calls)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/01de290...8b62780\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/position-independence\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Reducing the number of unlabeled memory references, to make life easier for modders. Also a requirement for using any other compiler on the reconstructed source code.\"\u003eposition-independence\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tWith no feedback to \u003ca href=\"/blog/2019-11-13\"\u003e📝 last week's blog post\u003c/a\u003e,\n\tI assume you all are fine with how things are going? Alright then, another\n\tone towards position independence, with the same approach as before…\n\u003cp\u003e\n\tSince \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e wanted to learn something about how the PC-98\n\tEGC is used in TH04 and TH05, I took a look at master.lib's \u003ccode\u003e\n\tegc_shift_*()\u003c/code\u003e functions. These simply do a hardware-accelerated\n\t\u003ccode\u003ememmove()\u003c/code\u003e of any VRAM region, and are used for screen shaking\n\teffects. Hover over the image below for the raw effect:\n\u003c/p\u003e\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2019-11-18-egc_shift_left-before.png?3f4592d7\"\u003e\u003cimg src=\"/blog/static/2019-11-18-egc_shift_left-before.png?3f4592d7\" onmouseover=\"this.setAttribute('src', \u0026#34;/blog/static/2019-11-18-egc_shift_left-after.png?bef83f56\u0026#34;);\" onmouseout=\"this.setAttribute('src', \u0026#34;/blog/static/2019-11-18-egc_shift_left-before.png?3f4592d7\u0026#34;);\" alt=\"Demonstration of an egc_shift_left() call\"\u003e\u003c/a\u003e\n\u003c/figure\u003e\u003cp\u003e\n\tThen, I finally wanted to take a look at the bullet structures, but it\n\trequired \u003ci\u003eway\u003c/i\u003e too much reverse-engineering to even start within ¾ of\n\ta position independence push. Even \u003ci\u003ewith\u003c/i\u003e the help of uth05win –\n\tbullet handling was changed quite a bit from TH04 to TH05.\n\u003c/p\u003e\u003cp\u003e\n\tWhat I ultimately settled on was more raw, \"boring\" PI work based around\n\tan already known set of functions. For this one, I looked at vector\n\tconstruction… and this time, that actually \u003ci\u003emade\u003c/i\u003e the games a little\n\tbit more position-independent, and \u003ci\u003ewasn't\u003c/i\u003e just all about removing\n\tfalse positives from the calculation. This was one of the few sets of\n\tfunctions that would also apply to TH01, and it revealed just how\n\tchaotically that game was coded. This one commit shows three ways how ZUN\n\tstored regular 2D points in TH01:\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\"regularly\", like in master.lib's \u003ccode\u003ePoint\u003c/code\u003e structure (X\n\tfirst, Y second)\u003c/li\u003e\n\t\u003cli\u003ereversed, (Y first and X second), then obviously with two distinct\n\tvariables declared next to each other\u003c/li\u003e\n\t\u003cli\u003eand with multiple points stored in a \u003ca href=\"https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_Arrays\"\u003estructure of arrays\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\t… yeah. But in more productive news, this \u003ci\u003edid\u003c/i\u003e actually lay the\n\tgroundwork for TH04 and TH05 bullet structures. Which might even be coming\n\tup within the next big, 5-push order from \u003ca class=\"customer\" href=\"https://thpatch.net/\"\u003eTouhou Patch Center\u003c/a\u003e? These are\n\tthe\tpriorities I got from them, let's see how close I can get!\n\u003c/p\u003e\u003cfigure\u003e\u003cembed\n\tsrc=\"/blog/static/2019-11-18-Priorities-given.svg?31174df8\" alt=\"Priorities given for P0060 to P0064\"\n\u003e\u003c/figure\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-29\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-13\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-11-18T21:28:01Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-11-13",
      "url": "https://rec98.nmlgc.net/blog/2019-11-13",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-11-13\"\u003e\u003ctime datetime=\"2019-11-13T23:49:07Z\"\u003e2019-11-13 23:49\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0057\"\u003eP0057\u003c/a\u003e\n\t\t\tTH04/TH05 PI (Items, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/1cb9731...ac7540d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0058\"\u003eP0058\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Items, part 2 + Midboss and boss variables, part 4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/ac7540d...fef0299\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e[Anonymous], \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/item\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Collectables, dropped from enemies and (mid)bosses.\"\u003eitem\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/meta\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Thoughts and news about this entire project.\"\u003emeta\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tSo, here we have the first two pushes with an explicit focus on position\n\tindependence… and they start out looking barely different from regular\n\treverse-engineering? They even already deduplicate a bunch of item-related\n\tcode, which was simple enough that it required little additional work?\n\tBecause the actual work, once again, was in comparing uth05win's\n\tinterpretations and naming choices with the original PC-98 code? So that\n\twe only ended up removing a handful of memory references there?\n\u003c/p\u003e\u003cp\u003e\n\t(Oh well, you can mod item drops now!)\n\u003c/p\u003e\u003cp\u003e\n\tSo, continuing to interpret PI as a mere by-product of reverse-engineering\n\tmight ultimately drive up the total PI cost quite a bit. But alright then,\n\tlet's systematically clear out some false positives by looking at\n\tmaster.lib function calls instead… and suddenly we get the PI progress we\n\twere looking for, nicely spread out over all games since TH02. That kinda\n\tmakes it sound like useless work, only done because it's dictated by some\n\tcounting algorithm on a website. But decompilation will want to convert\n\tall of these values to decimal anyway. We're merely doing that right now,\n\tacross all games.\n\u003c/p\u003e\u003cp\u003e\n\tThen again, it doesn't \u003ci\u003eactually\u003c/i\u003e make any game more\n\tposition-independent, and only proves how position-independent it already\n\twas. So I'm \u003ci\u003ereally\u003c/i\u003e wondering right now whether I should just rush\n\t\u003ci\u003eactual\u003c/i\u003e position independence by simply identifying structures and\n\ttheir sizes, and not bother with members or false positives until that's\n\tdone. That would certainly get the job done for TH04 and TH05 in just a\n\tfew more pushes, but then leave all the proving work (\u003ci\u003eand\u003c/i\u003e the road\n\tto 100% PI on the front page) to reverse-engineering.\n\u003c/p\u003e\u003cp\u003e\n\tI don't know. Would it be worth it to have a game that's \"\u003ci\u003emaybe\u003c/i\u003e\n\tfully position-independent\", only for there to \u003ci\u003emaybe\u003c/i\u003e be rare edge\n\tcases where it isn't?\n\u003c/p\u003e\u003cp\u003e\n\tOr maybe, continuing to strike a balance between identifying false\n\tpositives (fast) and reverse-engineering structures (slow) will continue\n\tto work out like it did now, and make us end up close to the current\n\testimate, which was attractive enough to sell out the crowdfunding for the\n\tfirst time… 🤔\n\u003c/p\u003e\u003cp\u003e\n\tPlease give feedback! If possible, by Friday evening UTC+1, before I start\n\tworking on the next PI push, this time with a focus on TH04.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-18\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-11-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-11-13T23:49:07Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-11-06",
      "url": "https://rec98.nmlgc.net/blog/2019-11-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-10-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-11-06\"\u003e\u003ctime datetime=\"2019-11-06T22:36:47Z\"\u003e2019-11-06 22:36\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0056\"\u003eP0056\u003c/a\u003e\n\t\t\tTH03 RE (The stolen sprite driver, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b28cefc...c09446a\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003erosenrose, [Anonymous]\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/thief\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Things that ZUN stole from others, without crediting them.\"\u003ethief\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tNo priorities, again…?! Please don't do this to me… 😕\n\u003c/p\u003e\u003cp\u003e\n\tWell, let's not continue with TH05 then 😛 And instead use the occasion to\n\tcommit \u003ca href=\"https://twitter.com/m1yur1/status/1018855232371998720\"\u003ethis\n\tinteresting discovery, made by @m1yur1 last year\u003c/a\u003e. Yup, TH03's \"ZUNSP\"\n\tsprite driver is actually a \"rebranded\" version of Promisence Soft's\n\t\u003ccode\u003eSPRITE16.COM\u003c/code\u003e. Sure, you \u003ci\u003ewere\u003c/i\u003e allowed to use this\n\tdriver in your own game, but replacing the copyright with your own isn't\n\texactly the nicest thing to do… That now makes three library programmers\n\tthat ZUN didn't credit. Makes me wonder what makes M. Kajihara so special.\n\tProbably the fact that Touhou has always been about the music for ZUN,\n\tfirst and foremost.\n\u003c/p\u003e\u003cp\u003e\n\tBut what makes this more than a piece of trivia is the fact that\n\tPromiscence Soft's SPRITE16 sample game \u003ci\u003eStormySpace\u003c/i\u003e was bundled\n\twith documentation on the driver. Shoutout to the Neo Kobe PC-98\n\tcollection for preserving he original release!\n\u003c/p\u003e\u003cp\u003e\n\tThat means more documented third-party code that we don't necessarily have\n\tto reverse-engineer, just like master.lib or KAJA's PMD driver. However,\n\tthe PC-98 EGC \u003ci\u003eis\u003c/i\u003e rather complex and \u003ci\u003edefinitely\u003c/i\u003e not designed\n\tfor alpha-tested 16-color sprite blitting. So it (once again) took quite a\n\twhile to make sense of SPRITE16's code and the available documentation on\n\tthe EGC, to come up with satisfying function names. As a result, I'm going\n\tto distribute the entire RE work related to TH03's SPRITE16 interface\n\tacross a total of three pushes, this one being the first of them.\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003eThe second one will reverse-engineer the SPRITE16 code reachable from\n\tits interrupt handler, and also come with somewhat detailed English\n\tdocumentation on the PC-98 EGC raster ops in particular,\u003c/li\u003e\n\t\u003cli\u003eand the third one will cover TH03's sprite drawing functions, which\n\tlook pretty undecompilable once again… (\u003cb\u003eUpdate\u003c/b\u003e:\n\t\u003ca href=\"/blog/2019-11-29\"\u003e📝 Delivered and \u003ci\u003eactually decompiled\u003c/i\u003e in P0060\u003c/a\u003e)\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tNext up, we do have more TH05 progress though, now for the first time\n\tspecifically with position independence in mind. Pretty excited for this\n\tone!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-13\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-10-14\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-11-06T22:36:47Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-10-14",
      "url": "https://rec98.nmlgc.net/blog/2019-10-14",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-10-14\"\u003e\u003ctime datetime=\"2019-10-14T23:13:21Z\"\u003e2019-10-14 23:13\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0036\"\u003eP0036\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Generic player shot functions) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/a533b5d...82b0e1d\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0037\"\u003eP0037\u003c/a\u003e\n\t\t\tTH05 decompilation (Yuuka's, Mima's, and Marisa's shot functions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/82b0e1d...e7e1cbc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tAnd just in time for \u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e's last outstanding pushes, the\n\tTH05 shot type control functions made the speedup happen!\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003e\n\t\tTH05 as a whole is now 20% reverse-engineered, and 50% position\n\t\tindependent,\n\t\u003c/li\u003e\u003cli\u003e\n\t\tTH05's \u003ccode\u003eMAIN.EXE\u003c/code\u003e is now even below TH02's in terms of not\n\t\tyet RE'd instructions,\n\t\u003c/li\u003e\u003cli\u003e\n\t\tand all price estimates have now fallen significantly.\n\t\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\tIt would have been \u003ci\u003ereally\u003c/i\u003e nice to also include Reimu's shot\n\tcontrol functions in this last push, but figuring out this entire system,\n\twith its weird bitflags and \u003ccode\u003eswitch\u003c/code\u003e statement\n\tmicro-optimizations, was once again taking way longer than it should\n\thave. Especially with my new-found insistence on turning this obvious\n\tcopy-pasta into something somewhat readable and terse…\n\u003c/p\u003e\u003cp\u003e\n\tBut with such a rather tabular visual structure, things should now be\n\tmoddable in hopefully easily consistent way. Of course, since we're\n\t\u003ci\u003eonly\u003c/i\u003e at 54% position independence for \u003ccode\u003eMAIN.EXE\u003c/code\u003e,\n\tthis isn't possible yet \u003ca href=\"/faq#pi-what\"\u003ewithout\n\tcrashing the game\u003c/a\u003e, but modifying damage would already work.\n\u003c/p\u003e\u003cp\u003e\n\tDespite my earlier claims of ZUN only having used C++ in TH01, as it's the\n\tonly game using \u003ccode\u003enew\u003c/code\u003e and \u003ccode\u003edelete\u003c/code\u003e,\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/blob/master/Research/Borland%20C%2B%2B%20decompilation.md\"\u003e\n\tit's now pretty much confirmed that ZUN used it for all games, as inlined\n\tfunctions (and by extension, C++ class methods) are the only way to get\n\tcertain instructions out of the Turbo C++ code generator\u003c/a\u003e. Also, I've\n\tkept my promise and started \u003ci\u003ereally\u003c/i\u003e filling that decompilation\n\tpattern file.\n\u003c/p\u003e\u003cp\u003e\n\tAnd now, with the reverse-engineering backlog finally being cleared out,\n\twe wait for the next orders, and the direction they might focus on…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-11-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-10-14T23:13:21Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-24",
      "url": "https://rec98.nmlgc.net/blog/2019-09-24",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-10-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-24\"\u003e\u003ctime datetime=\"2019-09-24T21:12:33Z\"\u003e2019-09-24 21:12\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0034\"\u003eP0034\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Player variables, part 2 + Deathbomb window)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6cdd229...6f1f367\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0035\"\u003eP0035\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Player rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6f1f367...a533b5d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/bomb\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Limited-use item that damages enemies and grants temporary invulnerability, while playing a flashy animation specific to the player character.\"\u003ebomb\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tDeathbombs confirmed, in both TH04 and TH05! On the surface, it's the same\n\t\u003ca href=\"https://www.youtube.com/watch?v=4du_rEB0M88\"\u003e8-frame window as in\n\tmost Windows games\u003c/a\u003e, but due to the slightly lower PC-98 frame rate of\n\t56.4\u0026nbsp;Hz, it's actually slightly more lenient in TH04 and TH05.\n\u003c/p\u003e\u003cp\u003e\n\tThe last function in front of the TH05 shot type control functions marks\n\tthe player's previous position in VRAM to be redrawn. But as it turns out,\n\t\"player\" not only means \"the player's option satellites on shot levels ≥\n\t2\", but also \"the explosion animation if you lose a life\", which required\n\treverse-engineering both things, ultimately leading to the confirmation of\n\tdeathbombs.\n\u003c/p\u003e\u003cp\u003e\n\tIt actually was kind of surprising that we then had reverse-engineered\n\teverything related to \u003ci\u003erendering\u003c/i\u003e all three things mentioned above,\n\tand could also cover the player rendering function right now. Luckily,\n\tTH05 didn't decide to also micro-optimize \u003ci\u003ethat\u003c/i\u003e function into\n\tun-decompilability; in fact, it wasn't changed at all from TH04. Unlike\n\tthe one invalidation function whose decompilation would have\n\t\u003ci\u003eactually\u003c/i\u003e been the goal here…\n\u003c/p\u003e\u003cp\u003e\n\tBut now, we've finally gotten to where we wanted to… and only got 2\n\toutstanding decompilation pushes left. Time to get the website ready for\n\thosting an actual crowdfunding campaign, I'd say – It'll make a better\n\timpression if people can still see things being delivered after the big\n\tannouncement.\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-10-14\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-24T21:12:33Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-21",
      "url": "https://rec98.nmlgc.net/blog/2019-09-21",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-21\"\u003e\u003ctime datetime=\"2019-09-21T12:11:18Z\"\u003e2019-09-21 12:11\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0031\"\u003eP0031\u003c/a\u003e\n\t\t\tTH05 decompilation (Stage .BB loading + Segment split research)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/dea40ad...9f764fa\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0032\"\u003eP0032\u003c/a\u003e\n\t\t\tTH02/TH04/TH05 RE (Score update and rendering)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9f764fa...e6294c2\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0033\"\u003eP0033\u003c/a\u003e\n\t\t\tTH04/TH05 RE (HUD bars + Player movement)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/e6294c2...6cdd229\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tasm\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo Assembler.\"\u003etasm\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/jank\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code of questionable quality.\"\u003ejank\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tThe glacial pace continues, with TH05's unnecessarily, inappropriately\n\tmicro-optimized, and hence, un-decompilable code for rendering the current\n\tand high score, as well as the enemy health / dream / power bars. While\n\tthe latter might still pass as well-written ASM, the former goes to such\n\tridiculous levels that it ends up being \u003ci\u003etechnically\u003c/i\u003e buggy. If you\n\tenjoy \u003ca href=\"https://twitter.com/zun_code\"\u003equality ZUN code\u003c/a\u003e, it's\n\tdefinitely worth a read.\n\u003c/p\u003e\u003cp\u003e\n\tIn TH05, this all still is at the end of code segment #1, but in TH04,\n\tthe same code lies all over the same segment. And since I \u003ci\u003ereally\u003c/i\u003e\n\twanted to move that code into its final form \u003ci\u003enow\u003c/i\u003e, I finally did the\n\tresearch into decompiling from anywhere else in a segment.\n\u003c/p\u003e\u003cp\u003e\n\tTurns out we actually can! It's kinda annoying, though: After splitting\n\tthe segment after the function we want to decompile, we then need to group\n\tthe two new segments back together into one \"virtual segment\" matching the\n\toriginal one. \u003ci\u003eBut\u003c/i\u003e since all ASM in ReC98 heavily relies on being\n\tassembled in MASM mode, we then start to suffer from MASM's group\n\taddressing quirk. Which then forces us to manually prefix every single\n\tfunction call\n\u003c/p\u003e\u003cul\u003e\n\t\u003cli\u003efrom inside the group\u003c/li\u003e\n\t\u003cli\u003eto anywhere else within the newly created segment\u003c/li\u003e\n\u003c/ul\u003e\u003cp\u003e\n\twith the group name. It's stupidly boring busywork, because of all the\n\tfunction calls you \u003ci\u003emustn't\u003c/i\u003e prefix. Special tooling might make this\n\teasier, but I don't have it, and I'm not getting crowdfunded for it.\n\u003c/p\u003e\u003cp\u003e\n\tSo while you now definitely \u003ci\u003ecan\u003c/i\u003e request any specific thing in any\n\tof the 5 games to be decompiled \u003ci\u003eright now\u003c/i\u003e, it will take slightly\n\tlonger, and cost slightly more.\u003cbr\u003e\n\t(Except for that one big segment in TH04, of course.)\n\u003c/p\u003e\u003cp\u003e\n\tOnly one function away from the TH05 shot type control functions now!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-21T12:11:18Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-15",
      "url": "https://rec98.nmlgc.net/blog/2019-09-15",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-15\"\u003e\u003ctime datetime=\"2019-09-15T19:29:47Z\"\u003e2019-09-15 19:29\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0029\"\u003eP0029\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Midboss and boss function names)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6ff427a...c7fc4ca\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0030\"\u003eP0030\u003c/a\u003e\n\t\t\tTH05 decompilation (Stage setup)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/c7fc4ca...dea40ad\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/tcc\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Discoveries about Turbo C++ 4.0J, the C++ compiler ZUN originally used for PC-98 Touhou.\"\u003etcc\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tHere we go, new C code! …eh, it will still take a bit to really get\n\tdecompilation going at the speeds I was hoping for. Especially with the\n\tsheer amount of \u003ci\u003estuff\u003c/i\u003e that is set in the first few significant\n\tfunctions we actually \u003ci\u003ecan\u003c/i\u003e decompile, which now all has to be\n\tcorrectly declared in the C world. Turns out I spent the last 2 years\n\tscrewing up the case of exported functions, and even some of their names,\n\tso that it didn't actually reflect their calling convention… yup. That's\n\tjust the stuff you tend to forget while it doesn't matter.\n\u003c/p\u003e\u003cp\u003e\n\tTo make up for that, I decided to research whether we can make use of some\n\tC++ features to improve code readability after all. Previously, it seemed\n\tthat TH01 was the only game that included any C++ code, whereas TH02 and\n\tlater seemed to be 100% C and ASM. However, during the development of the\n\tsoon to be released new build system, I noticed that even this old\n\tcompiler from the mid-90's, infamous for prioritizing compile speeds over\n\tall but the most trivial optimizations, was capable of quite surprising\n\tlevels of automatic inlining with class methods…\n\u003c/p\u003e\u003cp\u003e\n\t…leading the research to culminate in the mindblow that is\n\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/9d121c7\"\u003e\u003ccode\u003e9d121c7\u003c/code\u003e\u003c/a\u003e – yes, we \u003ci\u003ecan\u003c/i\u003e use C++ class methods\n\tand operator overloading to make the code more readable, while still\n\tgenerating the same code than if we had just used C and preprocessor\n\tmacros.\n\u003c/p\u003e\u003cp\u003e\n\tLooks like there's now the potential for a few pull requests from outside\n\tdevs that apply C++ features to improve the legibility of previously\n\tdecompiled and terribly macro-ridden code. So, if anyone wants to help\n\twithout spending money…\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-21\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-11\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-15T19:29:47Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-11",
      "url": "https://rec98.nmlgc.net/blog/2019-09-11",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-11\"\u003e\u003ctime datetime=\"2019-09-11T21:25:04Z\"\u003e2019-09-11 21:25\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0028\"\u003eP0028\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Midboss and boss variables, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6023f5c...6ff427a\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e\n\tBack to actual development! Starting off this stretch with something\n\tfairly mechanical, the few remaining generic boss and midboss state\n\tvariables. And once we start converting the constant numbers used for and\n\taround those variables into decimal, the estimated position independence\n\tprobability immediately jumped by 5.31% for TH04's \u003ccode\u003eMAIN.EXE\u003c/code\u003e,\n\tand 4.49% for TH05's – despite not having made the game any more position-\n\tindependent than it was before. Yup… lots of false positives in there, but\n\twho can really know for sure without having put in the work.\n\u003c/p\u003e\u003cp\u003e\n\tBut now, we've RE'd enough to finally decompile something again next,\n\t4 years after the last decompilation of anything!\n\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-11T21:25:04Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-09",
      "url": "https://rec98.nmlgc.net/blog/2019-09-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-09\"\u003e\u003ctime datetime=\"2019-09-09T13:11:53Z\"\u003e2019-09-09 13:11\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0016\"\u003eP0016\u003c/a\u003e\n\t\t\tWebsite (Price estimate, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/98b5090...bca833b\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0017\"\u003eP0017\u003c/a\u003e\n\t\t\tWebsite (Price estimate, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/3f81d1f...d5b9ea2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/theqp\"\u003eqp\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWebsite development time: 12/12\u003c/p\u003e\n\u003cp\u003eCalculating the average speed of the previous crowdfunded pushes, we arrive at estimated \"goals\" of…\u003c/p\u003e\n\n\u003cfigure\u003e\u003ca\nhref=\"/blog/static/2019-09-09-Estimate-at-60235fc.png?35f7dfa1\"\u003e\u003cimg src=\"/blog/static/2019-09-09-Estimate-at-60235fc.png?35f7dfa1\" alt=\"Crowdfunding estimate at 60235fc\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\n\n\u003cp\u003eSo, time's up, and I didn't even get to the entire PayPal integration and FAQ parts… 😕 Still got to clarify a couple of legal questions before formally starting this, though. So for now, let's continue with \u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e's next 5 TH05 reverse-engineering and decompilation pushes, and watch those prices go down a bit… hopefully quite significantly!\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-11\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-09-04\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-09T13:11:53Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-09-04",
      "url": "https://rec98.nmlgc.net/blog/2019-09-04",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-26\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-09-04\"\u003e\u003ctime datetime=\"2019-09-04T16:14:26Z\"\u003e2019-09-04 16:14\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0013\"\u003eP0013\u003c/a\u003e\n\t\t\tWebsite (Design)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/b9805d2...efeddd8\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0014\"\u003eP0014\u003c/a\u003e\n\t\t\tWebsite (Crowdfunding log + Blog, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/31474a0...9dc9632\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0015\"\u003eP0015\u003c/a\u003e\n\t\t\tWebsite (Blog, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/9dc9632...8d3652f\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/theqp\"\u003eqp\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWebsite development time: 10/12\u003c/p\u003e\n\u003cp\u003eIn order to be able to calculate how many instructions and absolute memory references are actually being removed with each push, we first need the database with the previous pushes from the Discord crowdfunding days. And while I was at it, I also imported the summary posts from back then.\u003c/p\u003e\n\u003cp\u003eAlso, we now got something resembling a web design!\u003c/p\u003e\n\n\u003cfigure class=\"side_by_side\"\u003e\u003ca\nhref=\"/blog/static/2019-09-04-Fundlog.png?3ad03c47\"\u003e\u003cimg src=\"/blog/static/2019-09-04-Fundlog.png?3ad03c47\" alt=\"Crowdfunding log\"\u003e\u003c/a\u003e\u003ca\nhref=\"/blog/static/2019-09-04-Blog.png?4f34868a\"\u003e\u003cimg src=\"/blog/static/2019-09-04-Blog.png?4f34868a\" alt=\"Blog\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-26\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-09-04T16:14:26Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-08-26",
      "url": "https://rec98.nmlgc.net/blog/2019-08-26",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-08-26\"\u003e\u003ctime datetime=\"2019-08-26T16:21:46Z\"\u003e2019-08-26 16:21\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0012\"\u003eP0012\u003c/a\u003e\n\t\t\tWebsite (Memory reference counting)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/b9918cc...b9805d2\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/theqp\"\u003eqp\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWebsite development time: 7/12\u003c/p\u003e\n\u003cp\u003eSo yeah, \"upper bound\" and \"probability\". In reality it's certainly better than the numbers suggest, but as I keep saying, we can't say much about position independence without having reverse-engineered everything.\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eNext up:\u003c/i\u003e Money goals.\u003c/p\u003e\n\n\u003cfigure class=\"side_by_side\"\u003e\u003ca\nhref=\"/blog/static/2019-08-26-MemRef-Upper-at-6023f5c.png?19da9cdd\"\u003e\u003cimg src=\"/blog/static/2019-08-26-MemRef-Upper-at-6023f5c.png?19da9cdd\" alt=\"Upper bound of remaining absolute memory references at 60235fc\"\u003e\u003c/a\u003e\u003ca\nhref=\"/blog/static/2019-08-26-Position-independence-at-6023f5c.png?8065d8a6\"\u003e\u003cimg src=\"/blog/static/2019-08-26-Position-independence-at-6023f5c.png?8065d8a6\" alt=\"Probability of position independence at 60235fc\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-09-04\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-24\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-08-26T16:21:46Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-08-24",
      "url": "https://rec98.nmlgc.net/blog/2019-08-24",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-26\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-08-24\"\u003e\u003ctime datetime=\"2019-08-24T22:10:53Z\"\u003e2019-08-24 22:10\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0011\"\u003eP0011\u003c/a\u003e\n\t\t\tWebsite (Completion percentages)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/40c1e98...b9918cc\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/theqp\"\u003eqp\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWebsite development time: 6/12\u003c/p\u003e\n\u003cp\u003eHere we go, overall ReC98 reverse-engineering progress. Now viewable for every commit on the page.\u003c/p\u003e\n\n\u003cfigure class=\"side_by_side\"\u003e\u003ca\nhref=\"/blog/static/2019-08-24-Stats-at-6023f5c.png?aafe46e3\"\u003e\u003cimg src=\"/blog/static/2019-08-24-Stats-at-6023f5c.png?aafe46e3\" alt=\"Number of not yet reverse-engineered x86 instructions at 60235fc\"\u003e\u003c/a\u003e\u003ca\nhref=\"/blog/static/2019-08-24-Percentages-at-6023f5c.png?0f4053bf\"\u003e\u003cimg src=\"/blog/static/2019-08-24-Percentages-at-6023f5c.png?0f4053bf\" alt=\"Reverse-engineering completion percentage at 60235fc\"\u003e\u003c/a\u003e\u003c/figure\n\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-26\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-08-23\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-08-24T22:10:53Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-08-23",
      "url": "https://rec98.nmlgc.net/blog/2019-08-23",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-03-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-08-23\"\u003e\u003ctime datetime=\"2019-08-23T21:43:10Z\"\u003e2019-08-23 21:43\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0010\"\u003eP0010\u003c/a\u003e\n\t\t\tWebsite (Golang HTML templating)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/cbda977...94127fb\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0054\"\u003eP0054\u003c/a\u003e\n\t\t\tWebsite (ASM instruction counting, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/94127fb...3161d7e\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0055\"\u003eP0055\u003c/a\u003e\n\t\t\tWebsite (ASM instruction counting, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/rec98.nmlgc.net/compare/3161d7e...40c1e98\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/DTM9025\"\u003eDTM\u003c/a\u003e, \u003ca class=\"customer\" href=\"https://opensrc.club/\"\u003eEgor\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/website\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code behind this website.\"\u003ewebsite\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWebsite development time: 5/12\u003c/p\u003e\n\u003cp\u003eNow with the number of not yet RE'd x86 instructions the you might have seen in the thpatch Discord. They're a bit smaller now, didn't filter out a couple of directives back then.\u003c/p\u003e\n\u003cp\u003eYes, requesting these currently is super slow. That's why I didn't want to have everyone here yet!\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eNext step:\u003c/i\u003e Figuring out the actual total number of game code instructions, for that nice \"% done\". Also, trying to do the same for position independence.\u003c/p\u003e\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-24\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-03-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-08-23T21:43:10Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-03-06",
      "url": "https://rec98.nmlgc.net/blog/2019-03-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-03-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-03-06\"\u003e\u003ctime datetime=\"2019-03-06T19:39:00Z\"\u003e2019-03-06 19:39\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0051\"\u003eP0051\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Midboss and boss explosions)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6ed8e60...3ba536a\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0052\"\u003eP0052\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Score variables)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6ed8e60...3ba536a\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0053\"\u003eP0053\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Text popups)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/6ed8e60...3ba536a\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/hud\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"The area of the screen that displays the current score, lives, and bombs.\"\u003ehud\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eBoss explosions! And… urgh, I \u003ci\u003ereally\u003c/i\u003e also had to wade through that overly complicated HUD rendering code. Even though I had to pick \u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e's 7th push here as well, the worst of that is still to come. TH04 and TH05 exclusively store the current and high score internally as unpacked little-endian BCD, with some pretty dense ASM code involving the venerable x86 BCD instructions to update it.\u003c/p\u003e\r\n\r\n\u003cp\u003eSo, what's actually the goal here. Since I was given no priorities \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":onricdennat:\" width=\"24\" height=\"24\" style=\"transform: scaleX(-1);\"\u003e, I still haven't had to (potentially) waste time researching whether we really can decompile from anywhere else inside a segment other than backwards from the end. So, the most efficient place for decompilation right now still is the end of TH05's \u003ccode\u003emain_01_TEXT\u003c/code\u003e segment. With maybe 1 or 2 more reverse-engineering commits, we'd have everything for an efficient decompilation up to \u003ccode\u003esub_123AD\u003c/code\u003e. And that mass of code just happens to include all the shot type control functions, and makes up 3,007 instructions in total, or 12% of the entire remaining unknown code in MAIN.EXE.\u003c/p\u003e\r\n\r\n\u003cp\u003eSo, the most reasonable thing would be to actually put some of the upcoming decompilation pushes towards reverse-engineering that missing part. I don't think that's a bad deal since it will allow us to mod TH05 shot types in C sooner, but \u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e and \u003ca class=\"customer\" href=\"https://github.com/theqp\"\u003eqp\u003c/a\u003e might disagree \u003cimg src=\"/static/emoji-thonk.png?0a583f91\" alt=\":thonk:\" width=\"24\" height=\"24\" \u003e\u003c/p\u003e\r\n\r\n\u003cp\u003eNext up: thcrap TL notes, followed by finally finishing GhostPhanom's old ReC98 future-proofing pushes. I \u003ci\u003ereally\u003c/i\u003e don't want to decompile without a proper build system.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-08-23\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-03-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-03-06T19:39:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-03-01",
      "url": "https://rec98.nmlgc.net/blog/2019-03-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-03-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-02-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-03-01\"\u003e\u003ctime datetime=\"2019-03-01T22:25:00Z\"\u003e2019-03-01 22:25\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0049\"\u003eP0049\u003c/a\u003e\n\t\t\tTH04/TH05 RE (.BB loading)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/893bd46...6ed8e60\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0050\"\u003eP0050\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Midboss and boss variables, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/893bd46...6ed8e60\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/yumeko\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"TH05\u0026#39;s Stage 5 boss.\"\u003eyumeko\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eSometimes, \"strategically picking things to reverse-engineer\" unfortunately also means \"having to move seemingly random and utterly uninteresting stuff, which will only make sense later, out of the way\". Really, this was \u003ci\u003eso boring\u003c/i\u003e. Gonna get a lot more exciting in the next ones though.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-03-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-02-28\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-03-01T22:25:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-02-28",
      "url": "https://rec98.nmlgc.net/blog/2019-02-28",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-03-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-01-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-02-28\"\u003e\u003ctime datetime=\"2019-02-28T16:58:00Z\"\u003e2019-02-28 16:58\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0047\"\u003eP0047\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Player shots, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9a2c6f7...893bd46\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0048\"\u003eP0048\u003c/a\u003e\n\t\t\tTH04/TH05 RE (8×8 sparks)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/9a2c6f7...893bd46\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/waste\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Consuming memory or disk space for no reason, or wrong reasons. Decent optimization opportunities for mods.\"\u003ewaste\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eSo, let's continue with player shots! …eh, or maybe not directly, since they involve two other structure types in TH05, which we'd have to cover first. One of them is a different sort of sprite, and since I like me some context in my reverse-engineering, let's disable every other sprite type first to figure out what it is.\u003c/p\u003e\r\n\r\n\u003cp\u003eOne of those other sprite types were the little sparks flying away from killed stage enemies, midbosses, and grazed bullets; easy enough to also RE right now. Turns out they use the same 8 hardcoded 8×8 sprites in TH02, TH04, and TH05. Except that it's actually 64 16×8 sprites, because ZUN wanted to pre-shift them for all 8 possible start pixels within a planar VRAM byte (rather than, like, just writing a few instructions to shift them programmatically), leading to them taking up 1,024 bytes rather than just 64.\u003cbr\u003e\r\nOh, and the thing I wanted to RE *actually* was the decay animation whenever a shot hits something. Not too complex either, especially since it's exclusive to TH05.\u003c/p\u003e\r\n\r\n\u003cp\u003eAnd since there was some time left and I actually have to pick some of the next RE places strategically to best prepare for the upcoming 17 decompilation pushes, here's two more function pointers for good measure.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-03-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2019-01-01\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-02-28T16:58:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2019-01-01",
      "url": "https://rec98.nmlgc.net/blog/2019-01-01",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-02-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2019-01-01\"\u003e\u003ctime datetime=\"2019-01-01T01:38:00Z\"\u003e2019-01-01 01:38\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0046\"\u003eP0046\u003c/a\u003e\n\t\t\tTH04/TH05 RE (16×16 sprite rendering + Player shots, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/612beb8...deb45ea\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eStumbled across one more drawing function in the way… which was only a duplicated and seemingly pointlessly micro-optimized copy of master.lib's \u003ccode\u003esuper_roll_put_tiny()\u003c/code\u003e function, used for fast display of 4-color 16×16 sprites.\u003c/p\u003e\r\n\r\n\u003cp\u003eWith this out of the way, we can tackle player shot sprite animation next. This will get rid of a lot of code, since every power level of every character's shot type is implemented in its own function. Which makes up thousands of instructions in both TH04 and TH05 that we can nicely decompile in the future without going through a dedicated reverse-engineering step.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-02-28\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-30\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2019-01-01T01:38:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-30",
      "url": "https://rec98.nmlgc.net/blog/2018-12-30",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-01-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-26\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-30\"\u003e\u003ctime datetime=\"2018-12-30T01:46:00Z\"\u003e2018-12-30 01:46\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0043\"\u003eP0043\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Scrolling stage backgrounds, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/261d503...612beb8\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0044\"\u003eP0044\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Scrolling stage backgrounds, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/261d503...612beb8\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0045\"\u003eP0045\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Scrolling stage backgrounds, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/261d503...612beb8\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eTurns out I had only been about half done with the drawing routines. The rest was all related to redrawing the scrolling stage backgrounds after other sprites were drawn on top. Since the PC-98 does have hardware-accelerated scrolling, but no hardware-accelerated sprites, everything that draws animated sprites into a scrolling VRAM must then also make sure that the background tiles covered by the sprite are redrawn in the next frame, which required a bit of ZUN code. And \u003ci\u003ethat\u003c/i\u003e are the functions that have been in the way of the expected rapid reverse-engineering progress that uth05win was supposed to bring. So, looks like everything's going to go really fast now?\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2019-01-01\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-26\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-30T01:46:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-26",
      "url": "https://rec98.nmlgc.net/blog/2018-12-26",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-26\"\u003e\u003ctime datetime=\"2018-12-26T17:57:00Z\"\u003e2018-12-26 17:57\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0025\"\u003eP0025\u003c/a\u003e\n\t\t\tTH04/TH05 RE (EGC calls)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0cde4b7...261d503\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0026\"\u003eP0026\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Boss backdrops)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0cde4b7...261d503\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0027\"\u003eP0027\u003c/a\u003e\n\t\t\tTH04/TH05 RE (.STD loading)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/0cde4b7...261d503\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/blitting\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Getting pixel data to and from VRAM.\"\u003eblitting\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/mod\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Pre-compiled mod downloads, changing all sorts of things in the binaries.\"\u003emod\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003e… yeah, no, we won't get very far without figuring out these drawing routines.\u003cbr\u003e\r\nWhich process data that comes from the .STD files.\r\nWhich has various arrays related to the background… including one to specify the scrolling speed. And wait, setting that to 0 actually is what starts a boss battle?\r\n\u003c/p\u003e\u003cp\u003e\r\n\tSo, have a TH05 Boss Rush patch: \u003ca class=\"download\" href=\"/blog/static/2018-12-26-TH05BossRush.zip?65209996\" data-kb=\"79.9\"\u003e2018-12-26-TH05BossRush.zip \u003c/a\u003e\r\n\tTheoretically, this should have also worked for TH04, but for some reason,\r\n\tthe Stage 3 boss gets stuck on the first phase if we do this?\r\n\u003c/p\u003e\r\n\u003cp\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/cf9f629\"\u003eHere's the diff for the Boss Rush.\u003c/a\u003e Turning it into a thcrap-style Skipgame patch is left as an exercise for the reader.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-30\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-16\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-26T17:57:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-16",
      "url": "https://rec98.nmlgc.net/blog/2018-12-16",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-26\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-16\"\u003e\u003ctime datetime=\"2018-12-16T23:34:00Z\"\u003e2018-12-16 23:34\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0023\"\u003eP0023\u003c/a\u003e\n\t\t\tTH05 RE (Lasers, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/807df3d...0cde4b7\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0024\"\u003eP0024\u003c/a\u003e\n\t\t\tTH05 RE (Lasers, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/807df3d...0cde4b7\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th01\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方靈異伝　～  The Highly Responsive to Prayers\"\u003eth01\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/laser\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Beams that can collide with the player. Sometimes difficult.\"\u003elaser\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/micro-optimization\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Needlessly complicated code, achieving minimal performance gains at the cost of maintainability.\"\u003emicro-optimization\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eActually, I lied, and lasers ended up coming with everything that makes reverse-engineering ZUN code so difficult: weirdly reused variables, unexpected structures within structures, and those TH05-specific nasty, premature ASM micro-optimizations that will waste a lot of time during decompilation, since the majority of the code actually was C, except for where it wasn't.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-26\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-12\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-16T23:34:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-12",
      "url": "https://rec98.nmlgc.net/blog/2018-12-12",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-12\"\u003e\u003ctime datetime=\"2018-12-12T01:52:00Z\"\u003e2018-12-12 01:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0042\"\u003eP0042\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Rank + TH05 lasers, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/f3b6137...807df3d\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/laser\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Beams that can collide with the player. Sometimes difficult.\"\u003elaser\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eLaser… is not difficult. In fact, out of the remaining entity types I checked, it's the easiest one to fully grasp from uth05win alone, as it's only drawn using master.lib's line, circle, and polygon functions. Everything else ends up calling… something sprite-related that needs to be RE'd separately, and which uth05win doesn't help with, at all.\u003c/p\u003e\r\n\r\n\u003cp\u003eOh, and since the speed of shoot-out lasers (as used by TH05's Stage 2 boss, for example) always depends on rank, we also got this variable now.\u003c/p\u003e\r\n\r\n\u003cp\u003eThis only covers the structure itself – uth05win's member names for the \u003ccode\u003eLASER\u003c/code\u003e structure were not only a bit too unclear, but also plain wrong and misleading in one instance. The actual implementation will follow in the next one.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-16\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-09\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-12T01:52:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-09",
      "url": "https://rec98.nmlgc.net/blog/2018-12-09",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-09\"\u003e\u003ctime datetime=\"2018-12-09T00:26:00Z\"\u003e2018-12-09 00:26\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0041\"\u003eP0041\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Midboss and boss variables, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/b03bc91...f3b6137\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/midboss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A single bigger enemy with a slightly more complicated, hardcoded script, occasionally fought halfway through a stage.\"\u003emidboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/boss\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"A bigger enemy with an extensive hardcoded script, fought at the end of a stage. Sometimes has personality.\"\u003eboss\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eSo, after introducing instruction number statistics… let's go for over 2,000 lines that won't show up there immediately \u003cimg src=\"/static/emoji-tannedcirno.png?24b6a61e\" alt=\":tannedcirno:\" width=\"24\" height=\"24\" \u003e That being (mid-)boss HP, position, and sprite ID variables for TH04/TH05. Doesn't sound like much, but it kind of is if you insist on decimal numbers for easier comparison with uth05win's source code.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-12\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-12-06\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-09T00:26:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-12-06",
      "url": "https://rec98.nmlgc.net/blog/2018-12-06",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-10-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-12-06\"\u003e\u003ctime datetime=\"2018-12-06T22:38:00Z\"\u003e2018-12-06 22:38\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0040\"\u003eP0040\u003c/a\u003e\n\t\t\tTH04/TH05 RE (GRCG calls + Circles)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/d7483c0...b03bc91\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/animation\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Colorful, crisp images that are played in a sequence.\"\u003eanimation\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eLet's start this stretch with a pretty simple entity type, the growing and shrinking circles shown during bomb animations and around bosses in TH04 and TH05. Which can be drawn in varying colors… wait, what's all this inlined and duplicated GRCG mode and color setting code? Let's move that out into macros too, it takes up too much space when grepping for constants, and will raise a \"wait, what was \u003ci\u003ethat\u003c/i\u003e I/O port doing again\" question for most people reading the code again after a few months.\u003c/p\u003e\r\n\r\n\u003cp\u003e🎉 With this push, we've also hit a milestone! Less than 200,000 unknown x86 instructions remain until we've completely reverse-engineered all of PC-98 Touhou.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-09\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-10-15\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-12-06T22:38:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-10-15",
      "url": "https://rec98.nmlgc.net/blog/2018-10-15",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-09-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-10-15\"\u003e\u003ctime datetime=\"2018-10-15T11:21:00Z\"\u003e2018-10-15 11:21\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0009\"\u003eP0009\u003c/a\u003e\n\t\t\tFinishing CDG/CD2 support (special offer)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/79cc3ed...141baa4\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://github.com/DTM9025\"\u003eDTM\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/file-format\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Structure of an on-disk file format.\"\u003efile-format\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWhile we're waiting for Bruno to release the next thcrap build with ANM header patching, here are the resulting commits of the ReC98 CDG/CD2 special offer purchased by \u003ca class=\"customer\" href=\"https://github.com/DTM9025\"\u003eDTM\u003c/a\u003e, reverse-engineering \u003ci\u003eall\u003c/i\u003e code that covers these formats.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-12-06\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-09-17\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-10-15T11:21:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-09-17",
      "url": "https://rec98.nmlgc.net/blog/2018-09-17",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-10-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-09-02\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-09-17\"\u003e\u003ctime datetime=\"2018-09-17T22:05:00Z\"\u003e2018-09-17 22:05\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0019\"\u003eP0019\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 RE (Input, part 1)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/c592464\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0020\"\u003eP0020\u003c/a\u003e\n\t\t\tTH03 RE (Input, part 3)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/cbe8a37\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0021\"\u003eP0021\u003c/a\u003e\n\t\t\tTH03 RE (Input, part 4)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/8dfc2cd\"\u003e\u003c/a\u003e\u003cbr\u003e\n\t\t\t\u003ca href=\"/fundlog#P0022\"\u003eP0022\u003c/a\u003e\n\t\t\tTH03/TH04/TH05 RE (Input, part 2)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/commit/79cc3ed\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th03\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方夢時空　～  Phantasmagoria of Dim.Dream\"\u003eth03\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/pc98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Details on PC-98 hardware and software.\"\u003epc98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/input\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Processing data entered from the keyboard or a joypad.\"\u003einput\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cblockquote\u003e\u0026gt; OK, let's do a quick ReC98 update before going back to thcrap, shouldn't take long\r\n\u0026gt; Hm, all that input code is kind of in the way, would be nice to cover that first to ease comparisons with uth05win's source code\r\n\u0026gt; What the hell, why does ZUN do this? Need to do more research\r\n\u0026gt; …\r\n\u0026gt; OK, research done, wait, what are those other functions doing?\r\n\u0026gt; Wha, everything about this is just ever so slightly awkward\r\n\u003c/blockquote\u003e\r\n\r\n\u003cp\u003eWhich ended up turning this one update into 2/10, 3/10, 4/10 and 5/10 of \u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e's reverse-engineering commits. But at least we now got all shared input functions of TH02-TH05 covered and well understood.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-10-15\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-09-02\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-09-17T22:05:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-09-02",
      "url": "https://rec98.nmlgc.net/blog/2018-09-02",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-09-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-04-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-09-02\"\u003e\u003ctime datetime=\"2018-09-02T19:33:00Z\"\u003e2018-09-02 19:33\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0018\"\u003eP0018\u003c/a\u003e\n\t\t\tTH04/TH05 RE (Player variables, part 1 + Motion structure) \n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/746681d...178d589\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"https://twitter.com/zorg_z0rg_z0r8\"\u003ezorg\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/score\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Points collected during gameplay, and stored in high score files.\"\u003escore\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/uth05win\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Open-source Windows rewrite of TH05, based on reverse-engineered code from the original. Slightly inaccurate in places, but overall still a great help for this project.\"\u003euth05win\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eWhat do you do if the TH06 text image feature for thcrap should have been done 3 days™ ago, but keeps getting more and more complex, and you have a ton of other pushes to deliver anyway? Get some distraction with some light ReC98 reverse-engineering work. This is where it becomes very obvious how much uth05win helps us with \u003ci\u003eall\u003c/i\u003e the games, not just TH05.\u003c/p\u003e\r\n\r\n\u003cp\u003e\u003ca href=\"https://github.com/nmlgc/ReC98/commit/5a5c347\"\u003e\u003ccode\u003e5a5c347\u003c/code\u003e\u003c/a\u003e is the most important one in there, this was the missing substructure that now makes every other sprite-like structure trivial to figure out.\u003c/p\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-09-17\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003ca href=\"#2018-04-21\" title=\"Previous post\"\u003e🔽\u003c/a\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-09-02T19:33:00Z"
    },
    {
      "id": "https://rec98.nmlgc.net/blog/2018-04-21",
      "url": "https://rec98.nmlgc.net/blog/2018-04-21",
      "content_html": "\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-09-02\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003cdiv title=\"This is the first blog post.\"\u003e🔽\u003c/div\u003e\u003c/div\u003e\u003cdl\u003e\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e📝\u003c/span\u003e Posted:\u003c/dt\u003e\n\n\t\u003cdd\u003e\u003ca href=\"/blog/2018-04-21\"\u003e\u003ctime datetime=\"2018-04-21T13:52:00Z\"\u003e2018-04-21 13:52\u0026nbsp;UTC\u003c/time\u003e\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdiv class=\"post_meta\"\u003e\n\t\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🚚\u003c/span\u003e Summary of:\u003c/dt\u003e\n\t\t\u003cdd\u003e\n\t\t\t\u003ca href=\"/fundlog#P0008\"\u003eP0008\u003c/a\u003e\n\t\t\tTH02/TH04/TH05 RE (Shot variables and function pointers)\n\t\t\t\u003ca href=\"https://github.com/nmlgc/ReC98/compare/47e5601...d62dd06\"\u003e\u003c/a\u003e\u003c/dd\u003e\n\t\u003c/div\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e💰\u003c/span\u003e Funded by:\u003c/dt\u003e\n\t\u003cdd\u003e\u003ca class=\"customer\" href=\"http://lunarcast.net/\"\u003e-Tom-\u003c/a\u003e\u003c/dd\u003e\n\n\t\u003cdt\u003e\u003cspan class=\"emoji\"\u003e🏷️\u003c/span\u003e Tags:\u003c/dt\u003e\n\t\u003cdd\u003e\u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th02\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方封魔録　～  the Story of Eastern Wonderland\"\u003eth02\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th04\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方幻想郷　～  Lotus Land Story\"\u003eth04\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/th05\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"東方怪綺談　～  Mystic Square\"\u003eth05\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/rec98\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"PC-98 Touhou source code reconstruction.\"\u003erec98\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/gameplay\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Code that defines the main in-game experience.\"\u003egameplay\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/player\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Player-controlled characters.\"\u003eplayer\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003cspan class=\"tag\"\u003e\u003cform action=\"/blog/tag/shot\" method=\"POST\"\u003e\u003cbutton type=\"submit\" title=\"Projectiles fired by the player.\"\u003eshot\u003c/button\u003e\u003c/form\u003e\u003c/span\u003e \u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003cp\u003eYou could use this to get a homing Mima, for example.\u003c/p\u003e\r\n\r\n\u003cfigure\u003e\u003crec98-video class=\"rec98-player\"\u003e\u003cvideo poster=\"/blog/static/video/poster/2018-04-21-Homing_Mima.webp?19b2d9e0\" preload=\"none\" controls loop width=\"640\" height=\"400\" data-fps=\"40\" data-frame-count=\"235\" style=\"aspect-ratio: 640 / 400\" data-lossless=\"/blog/static/video/zmbv/2018-04-21-Homing_Mima.avi?3ebed3db\"\u003e\u003csource src=\"/blog/static/video/av1/2018-04-21-Homing_Mima.webm?025ce19a\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp9/2018-04-21-Homing_Mima.webm?46376284\" type=\"video/webm\"\u003e\u003csource src=\"/blog/static/video/vp8/2018-04-21-Homing_Mima.webm?780c1e8a\" type=\"video/webm\"\u003eVideo of Mima with Reimu's shot type. \u003ca href=\"/blog/static/video/zmbv/2018-04-21-Homing_Mima.avi?3ebed3db\"\u003eDownload\u003c/a\u003e\u003c/video\u003e\u003crec98-parent-init\u003e\u003c/rec98-parent-init\u003e\u003c/rec98-video\u003e\u003c/figure\u003e\r\n\n\u003cdiv class=\"nav\"\u003e\u003ca href=\"#2018-09-02\" title=\"Next post\"\u003e🔼\u003c/a\u003e\u003cdiv title=\"This is the first blog post.\"\u003e🔽\u003c/div\u003e\u003c/div\u003e\n\n",
      "date_published": "2018-04-21T13:52:00Z"
    }
  ]
}
