← Back

Reviving NiCai Copycat Phone Era Games on Modern Platforms

Reviving NiCai Copycat Phone Era Games on Modern Platforms
Contents

Project Name: nieche-emu (CoolBar .CBE High-Level Emulator)
Repository: github.com/nieche-cbe-emu
Supported Platforms: macOS (SwiftUI) / Android (Kotlin + Chaquopy) / Windows (Qt 6; plus an experimental Win32 frontend) / Web (local browser frontend for development)
Core Architecture: Dual-track differential architecture: Python (reference) + Rust (production)
Codebase Scale: 141 source files, 35,717 lines of code (physical lines, excluding vendored dependencies and build caches)
Current Status: All 35/35 modules in the corpus boot and reach their main loops; zero divergence in 300-frame differential testing


I. Background and Target Platform

Around 2010, a wave of domestic Chinese feature phones, epitomized by the NiCai I9, became widespread on the market. Unlike the then-dominant MediaTek (MTK) platform and its MRE (.vxp) ecosystem, these handsets were built on MStar Semiconductor’s MSW8533 / 7835 chipsets with an ARM926EJ-S processor, and ran the CoolBar (51coolbar / 酷吧) application platform, whose executable modules use the .CBE (CoolBar Executable) format.

A library of mechanically complete mobile games, each only a few megabytes or smaller, was built for CoolBar. Examples include the action RPG GodWar (众神之战), the stat-based dungeon crawler Magic Tower (魔塔), the top-down shooter Gear of War (战争机器), the side-scrolling action game Demon Castle (恶魔城), the vertical-scrolling air-combat shooter Ace Air Combat (皇牌空战), the martial-arts fighting game My Own Swordsman (武林外传), and the strategy game Three Kingdoms (三国志).

With the hardware at the end of its life, the official servers shut down, and the platform entirely closed-source with no public SDK documentation or interface specifications, this software had become impossible to run. The nieche-emu project uses black-box reverse engineering and firmware analysis to build a high-level emulator (HLE) that faithfully recreates the platform’s runtime environment and interaction on modern operating systems.


II. Overall System Architecture

The core architectural paradigm of CoolBar is: applications ship with their own game engines (including the DreamFactory engine, XS_* script virtual machines, and the GameLcd graphics layer) and dynamically register callbacks into the host’s Manager tables during initialization; the host OS provides low-level abstractions (LCD framebuffers, file I/O, dynamic memory management, input polling, hardware timers, and audio playback).

Consequently, the emulator does not need to simulate the game engine internally. Instead, it adopts a High-Level Emulation (HLE) design: an instruction-level CPU core (Unicorn Engine) provides ARM execution and memory virtualization, while host OS services are implemented natively in modern programming languages.

text
┌────────────────────────────────────────────────────────────────────────┐
│                        User Interface Frontends                        │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────────┐  │
│  │  macOS (SwiftUI) │  │  Android (Kotlin)│  │  Windows (Qt 6)      │  │
│  │  Process-Isolated│  │  Touch & Keypad  │  │  Native Window       │  │
│  └─────────┬────────┘  └─────────┬────────┘  └──────────┬───────────┘  │
└────────────┼─────────────────────┼──────────────────────┼──────────────┘
             │                     │ Chaquopy + ctypes    │
             ▼                     ▼                      ▼
┌────────────────────────────────────────────────────────────────────────┐
│                       Facade Layer (Session / C ABI)                   │
│   emu/host.py (Python Session)  /  rust/emuffi (libnieche C ABI)       │
│   Unified handling: input bitmask shaping, frame ticks, serialization  │
└──────────────────────────────────┬─────────────────────────────────────┘
                                   │
                                   ▼
┌────────────────────────────────────────────────────────────────────────┐
│                Core Emulation Layer (Dual-Track Python / Rust)         │
│  ┌─────────────────────────────────┐ ┌──────────────────────────────┐  │
│  │      Unicorn Engine (ARMv5TE)   │ │       HLE Host OS Services   │  │
│  │ - ARM926EJ-S Core (LE / BE-32)  │ │ - 27 Manager Method Tables   │  │
│  │ - RWPI / SB-relative Memory Map │ │ - VFS (Real FAT FS Overlay)  │  │
│  │ - Trap Interception & Stubs     │ │ - Framebuffer (240x400 565)  │  │
│  │ - Address 0 Zero Guard & Faults │ │ - Timers & Audio Events      │  │
│  └─────────────────────────────────┘ └──────────────────────────────┘  │
└──────────────────────────────────┬─────────────────────────────────────┘
                                   │
                                   ▼
┌────────────────────────────────────────────────────────────────────────┐
│               Binary Extraction & Codec Layer (cbelib)                 │
│  CBE Containers │ Multi-Package Parsing │ 9-bit LZ77 │ Graphic Codecs  │
└────────────────────────────────────────────────────────────────────────┘

1. Container Structure

.CBE binaries exist as either single-package containers or multi-package archives (used by Demon Castle, Lost in Island, Gear of War, and Mr. Zombie, among others):

  • Header: Contains magic markers, load base address (load_base, 0 for PIC modules), RO segment offset and size, RW segment size, and non-PIC image boundaries (image_end);
  • RO Segment: Compiled Thumb/ARM machine instructions and constants (.text + .rodata);
  • RW/ZI Segment: Writable global state (initialized .data plus zero-initialized .bss);
  • Resource Index & Payloads: Each entry begins with a 1-byte type tag (type 0 raw RGB565, type 1 GIF variant, type 2 compressed scripts, type 3 PNG, type 5/10 text/MIDI).

2. Memory Topology

The emulator partitions the 32-bit virtual address space into isolated regions:

text
0x0000_0000 ┌──────────────────────────────────────┐
            │ NULL_GUARD (128 KB, Zeroes + Hook)   │ Defends against null dereferences
0x0100_0000 ├──────────────────────────────────────┤
            │ RO Segment (Code, RWX)               │ PIC module code (non-PIC at load_base)
0x2000_0000 ├──────────────────────────────────────┤
            │ RW Segment (Globals, sb/r9 target)   │ Module data area
0x3000_0000 ├──────────────────────────────────────┤
            │ STACK (1 MB, grows downward)         │ Guest call stack
0x4000_0000 ├──────────────────────────────────────┤
            │ HEAP (256 MB dynamic memory)         │ Guest malloc / free
0x5000_0000 ├──────────────────────────────────────┤
            │ TRAP Area (HLE Interception Stubs)   │ One Thumb bx lr per slot
0x5100_0000 ├──────────────────────────────────────┤
            │ NATIVE Area (Native Thumb Stubs)     │ Constant getters, no host exit
0x6000_0000 ├──────────────────────────────────────┤
            │ DATA (Host Structures, 1st Page X)   │ Host Manager tables / context
            └──────────────────────────────────────┘

III. Codebase Scale and Distribution

The project comprises reverse-engineering tools, a Python reference implementation, a high-performance Rust production engine, and native frontends for three operating systems.

Excluding third-party dependencies, build artifacts, and binary assets, the codebase spans 141 source files and 35,717 lines of code (physical lines, including 2,260 lines of vmspec.py / vmspec.rs generated from DWARF):

1. Language Breakdown

LanguageLines of Code (LOC)PercentageCore Responsibilities
Rust15,37943.1%Production emulator (emucore), C ABI layer (emuffi), experimental Win32 app (emuwin)
Python14,79841.4%Reference implementation (emu/), reversing & differential test tools (tools/)
Swift2,1906.1%macOS native SwiftUI app (swiftapp/), early dot-matrix font generator (tools/fontgen/mkfont.swift)
C / C++1,8135.1%Windows Qt 6 frontend (qtapp/), core C ABI header (nieche.h)
Kotlin1,5374.3%Android native app (emu-android/): UI, virtual keypad, memory editor
Total35,717100%141 source files

2. Component and Directory Breakdown

text
nieche-emu/
├── rust/ (15,785 LOC / 46 files)
│   ├── emucore/           Rust core: machine state, memory allocators, HLE dispatcher, blitters
│   ├── emuffi/            Standard C ABI export (libnieche.{dylib,a}) and engine IPC channels
│   ├── emuwin/            Experimental lightweight native Windows (Win32) frontend (not shipped)
│   └── cbelib/            Container parser, LZ77 decompressor, and image codecs (only depends on pure-Rust miniz_oxide)
├── emu/ (8,926 LOC / 17 files)
│   ├── machine.py         Unicorn wrapper, memory segmentation, trap dispatcher, fault handlers
│   ├── runtime.py         Host context setup, main loop scheduling, screen stack, timer queue
│   ├── api.py             HLE implementations for 27 managers registered by firmware symbols
│   ├── oldlib.py          Legacy SDK game library (GAME_lcd, DF_PictureLibrary, ...), ported from the copy built into *Dynamic Dice (动感骰子)*
│   ├── vmspec.py          Auto-generated spec of 27 Managers / 998 method slots from firmware DWARF
│   ├── lcd.py / images.py Framebuffer management, clipping Blit operations, color palettes
│   └── vfs.py             Union VFS mounting actual phone FAT partition images
├── tools/ (4,215 LOC / 42 files)
│   ├── axf.py             DWARF2 symbol and DIE query tool for MStar firmware (wraps llvm-dwarfdump)
│   ├── baseline.py        Dual-track differential testing suite (frame and call-trace loggers)
│   ├── cbedis.py / cbex.py Virtual address disassembler, CBE resource archive unpackers
│   ├── keyprobe.py        Bitwise scanner to discover undocumented module keymasks
│   └── godwar.py          Touch navigation script for automated in-game map progression
├── emu-android/ (2,304 LOC / 9 files)
│   └── app/src/main/      Native Android UI: Chaquopy glue, touch mapping, frame pool, keypad, memory editor
├── swiftapp/ (2,081 LOC / 8 files)
│   └── *.swift            macOS SwiftUI UI: engine process pipes, game library and mirrors, Scale2x/3x
├── qtapp/ (1,710 LOC / 15 files)
│   └── src/               Windows Qt 6 UI: high-performance frame canvas and input events
└── cbelib/ (696 LOC / 4 files)
    └── Python container decoder, image converters (RGB565/GIF/PNG), and 9-bit LZ77 engine

Additionally, the repository maintains spec/vm_managers.json (4,399 lines of machine-extracted DWARF structure definitions) and 35 baseline execution traces in spec/baseline/ used for regression testing.


IV. AI Agent-Assisted Reverse Engineering and Engineering Practice

AI agents were integrated directly into the engineering workflow, handling symbol indexing, data-flow reconstruction, specification code generation, and differential debugging triage.

text
┌────────────────────────────────────────────────────────────────────────┐
│                        AI Agent Collaboration Workflow                 │
│                                                                        │
│  [Firmware AXF / ELF] ──► [DWARF Parsing & Symbols]   ─► Auto-Generate │
│                                ▲                         vmspec.py/rs  │
│                                │ Agent-Written Extraction Scripts      │
│                                                                        │
│  [Disassembly Traces] ──► [Calling Convention Deduce] ─► Parameter Map │
│                                ▲                         & Keymasks    │
│                                │ Register Dependency Tracing           │
│                                                                        │
│  [Differential Dumps] ──► [Divergence Root Cause]     ─► First Diverg- │
│                                ▲                         ing Call      │
│                                │ Call-Trace & Memory Comparison        │
│                                                                        │
│  [Python Prototype]   ──► [Isomorphic Rust Port]      ─► Production    │
│                                                          Rust Core     │
└────────────────────────────────────────────────────────────────────────┘

1. Firmware DWARF Parsing and Manager Specification Automation

The raw DWARF symbol dumps from the firmware image were massive (spec/vm_managers.json spans over 4,300 lines). AI agents helped design and refine the parsing pipeline, extracting exact C structure layouts across 27 Managers and 998 function pointer slots to automatically generate emu/vmspec.py and vmspec.rs:

python
# emu/vmspec.py: entry table auto-generated from DWARF (excerpt)
SYS = {
    0x004: ('VMGetIoManager', 'VmIoManagerTag'),
    0x00c: ('VMGetLcdManager', 'VmLcdManagerTag'),
    0x014: ('VMGetTimeManager', 'VmTimeManagerTag'),
    0x034: ('VMGetScreenManager', 'VmScreenManagerTag'),
    0x084: ('VMGetGameManagerOld', 'GameManagerOldTag'),
    # ... 25 Get entries in total; each Manager's method slots live in the MGR table
}

2. Disassembly Data-Flow Analysis and Semantics Deduction

For the legacy SDK syscall dispatch (vmDlSdk_old), which completely lacked debugging symbols, manually inspecting 664 call sites was prohibitively slow (the firmware itself only shipped the new SDK, so it could not help). AI agents assisted in tracking register flow trees across Capstone disassembly:

  • Determining parameter packing: Identifying that multi-argument wrappers stm their arguments into a global memory frame and read the return value back from that frame after the call, formalized as the width-aligned _old_unpack decoder (the numeric syscalls themselves are reconstructed in runtime.py);
  • Resolving boolean parameter polarity (the same problem on the new-SDK side): In Storage_Date, comparing the data flow around GodWar’s two call sites (buffer filled before the call vs. buffer parsed after the call) confirmed that argument 4 is isLoad rather than isWrite, correcting an earlier guess based on argument position;
  • Eliminating dead code: Of the 46 function numbers recovered statically, 11 had wrappers but were unreachable from any live path; once confirmed as dead code they were dropped, leaving 35 active syscalls.

3. Pinpointing Differential Divergence

During the Rust port, an AI agent compared per-frame call traces exported by both implementations; the first diverging call points straight at the faulty interface. Two typical cases:

  • A real divergence: Diamond Twister 3 (钻石迷情3) diverged at frame 188. The call sequences showed the Rust side’s paint callback being cut short: an ARM semihosting call (svc 0xab) left in the module’s C library had no interrupt hook on the Rust side, so Unicorn raised an exception and aborted the callback. Adding semihosting support made every frame match;
  • A false divergence: Gear of War once appeared to diverge at frame 260. This turned out to be a counting bug in the comparison script (it also counted “framebuffer differs, call sequence matches” as a pass), which had let several baselines’ framebuffer fingerprints go stale. After fixing the script and regenerating the baselines, the whole corpus matched frame for frame;
  • Both avoided tedious manual step-by-step memory debugging.

4. Cross-Language Semantic Verification

Translating from dynamic Python to strictly typed Rust introduces subtle behavioural risks (e.g., arithmetic truncation, endianness assumptions, sign extension). AI agents performed line-by-line semantic audits of the ported routines, verifying that defensive engineering rules (e.g., 4-pixel stride padding, ASCII open modes, -1 error handles) were faithfully preserved.


V. Core Reverse Engineering and Technical Deep Dives

This section details the critical reverse-engineering challenges and bug investigations resolved during the development of the emulator.

1. Memory Models, Runtime Layout, and Isolation

(1) Non-PIC Segment Placement Traps

Non-position-independent modules have fixed load addresses. Their RW segment cannot simply be placed at the next page boundary following the RO segment; its exact location is determined by image_end in the container header. In My Own Swordsman V10, miscalculating this by 0x2e4 bytes caused statically initialized VmScreen structures in .data to map into zeroed .bss, preventing the main loop from starting.

python
# emu/machine.py: Resolving RW base for non-PIC binaries
@staticmethod
def rw_place(mod):
    end = getattr(mod, "image_end", 0) or 0
    lo = mod.load_base
    hi = mod.load_base + max(mod.image_size, len(mod.ro)) + 0x10000
    if lo < end < hi:
        return end
    return mod.load_base + align_up(len(mod.ro), 16)

(2) Preserving r9 (Static Base) Across Host Callbacks

Under the ARM RVCT RWPI convention, r9 is the static base register, and the real firmware does not reset it when calling back into a module. Three Kingdoms boots via an outer launcher that does not use r9; after unpacking and jumping into the nested game, it points r9 to the inner game’s data section. If the host resets r9 to the launcher’s rw_base on every host callback, the inner game reads all globals as zero, triggering immediate null pointer crashes.

python
# Maintaining live r9 state across host boundaries
self.r9_live = self.rw_base  # Initialized to load base, evolves with guest execution

(3) Zero-Filling Address 0

Modules frequently invoke unpopulated function pointers (NULL). To prevent simulation aborts, a 128 KB guard page is mapped at 0x0. Early versions filled this region with ARM bx lr instructions (bytes 1e ff 2f e1) to turn null calls into no-ops.

This broke data parsing in modules like AppStore.CBE. The binary treats null pointers not only as callable hooks, but also as data structures (e.g., reading p->count). Reading the instruction bytes 1e ff 2f e1 as a length field produced a huge length and a run of out-of-bounds reads.

  • The correct approach: Address 0x0 must be filled entirely with 0x00, enabling the guest’s internal null-check logic (if (!p->count) return;) to execute properly;
  • Call interception: Execution attempts in this range are captured via Unicorn’s UC_HOOK_CODE. The handler records LR, sets the return value to 0 and calls emu_stop(); the outer driver then restarts execution at LR (changing PC inside the hook has no effect).
python
# emu/machine.py: Intercepting execution on address 0
def _on_null_call(self, uc, address, size, user):
    lr = uc.reg_read(UC_ARM_REG_LR)
    self.null_calls[lr & ~1] += 1
    self.setreg(0, 0)           # a null call returns 0
    self._resume_pc = lr        # writing PC inside the hook has no effect: remember where to resume
    uc.emu_stop()               # the outer call() restarts emu_start from LR

(4) Preventing Zero-Slide in Big-Endian Mode

For the five Big-Endian (BE-32) modules in the corpus (both editions of My Own Swordsman, WaiWai Cat, Three Kingdoms and Three Kingdoms!), ARM decodes a stream of zeroes (0x00000000) as:

asm
andeq r0, r0, r0    ; Machine code 00 00 00 00, effectively a NOP

If the host data area (DATA_BASE) is executable, a wild jump into uninitialized zeroed memory causes the CPU to slide through megabytes of NOPs before crashing out of bounds, destroying the original crash context.

To prevent this, the host structure area is mapped non-executable (UC_PROT_READ | UC_PROT_WRITE), with execution rights granted strictly to the first 4 KB page where the entry trampoline lives.


2. Dual SDK ABI Collision and Dynamic Trampoline Dispatch

The coexistence of two generations of SDKs created a direct contradiction at the entry point RO+0:

  • New SDK: entry(VmSysCallRegParam *p) — The module writes its AppStart and AppStop callbacks into *p;
  • Old SDK: entry(syscall_fn) — The module treats r0 as a syscall entrypoint and invokes it via bx r0.

My Own Swordsman V10 (module name fighter) does both at its entry point: it stores the argument in a global and also writes callbacks into the structure it points to, so static heuristics cannot tell the two apart reliably.

Solution: A hardware-level ARM trampoline placed at the host context base.

python
# emu/runtime.py: Dynamic entry trampoline in boot() (excerpt)
def boot(self):
    m = self.mach
    self.style = self.entry_style()

    # 1. Register legacy syscall trap
    tramp = m.new_trap("OldSysCall", self._old_syscall)
    
    # 2. Write an ARM branch instruction at host+0:
    # 0xE51FF004 -> ldr pc, [pc, #-4], which loads the word immediately following it into PC
    m.w32(self.host + 0x00, 0xE51FF004)
    m.w32(self.host + 0x04, tramp)

    # 3. Execute module entrypoint with host address in r0
    r = m.call(m.ro_base | 1, [self.host])

    # 4. Check if the memory header was overwritten
    cb0 = m.r32(self.host + 0x00)
    if cb0 != 0xE51FF004:
        # Instruction replaced: confirms New SDK; extract AppStart and AppStop
        self.mod_cb0 = cb0
        self.mod_cb1 = m.r32(self.host + 0x04)
    # If unchanged, confirms Old SDK; module retains self.host as its syscall handler
    return r
  • New modules run str rX, [r0], overwriting the trampoline with valid callback addresses;
  • Old modules execute blx host, run the ldr pc instruction, and are redirected straight into OldSysCall.

3. Reversing Legacy SDK Numeric System Calls

The legacy SDK (vmDlSdk_old) routes all interactions through 35 active numeric syscalls, passing arguments via memory frames and returning objects through memory descriptors.

(1) Syscall 1950: Application Registration

Modules invoke syscall(1950, blk) where blk is an argument frame:

  • +0x00: Main loop callback mod_cb0;
  • +0x04: Exit callback mod_cb1;
  • +0x08: Output slot for host helper function pointer.

If the host writes status code 1 into +0x08, the module executes ldr r0, [r4, #8]; bx r0. Masking off the Thumb bit (1 & ~1 = 0) branches to address 0. The host must populate +0x08 with a valid trap stub and return a valid context pointer in r0.

(2) Syscall 2001: Descriptor Handle Writeback

To fetch manager tables, legacy modules invoke:

c
h = syscall(143, 0);                 // Request manager, obtain temporary handle
desc = { &out_ptr, h, 4 };           // Construct fetch descriptor
syscall(2001, &desc);                // Host resolves handle and writes instance into *out_ptr

If the host only writes the return value to register r0, out_ptr remains null, triggering immediate crashes on subsequent table lookups. The host must write the value directly into the guest pointer:

python
# emu/runtime.py: Syscall 2001 handle resolution
def _old_fetch(self, mc):
    desc = mc.arg(1)
    if not desc:
        mc.ret(0); return
    ptr, handle, ln = mc.r32(desc), mc.r32(desc + 4), mc.r32(desc + 8)
    if ptr and ln >= 4:
        mc.w32(ptr, handle)
    elif ptr and ln == 2:
        mc.uc.mem_write(ptr, (handle & 0xFFFF).to_bytes(2, 'little' if mc.le else 'big'))
    elif ptr and ln == 1:
        mc.uc.mem_write(ptr, bytes([handle & 0xFF]))
    mc.ret(1)

(3) Undocumented SMS Address Arithmetic

In billing routines, disassembled code revealed an unusual firmware version check:

asm
; My Own Swordsman (New Edition) RO+0x764c
ldr r0, [r4, #0x244]    ; Address of GetRemainDay in vtable
ldr r1, [r4, #0x240]    ; Address of GetPayNum in vtable
subs r2, r0, r1         ; Compute address delta

The binary uses this delta to identify firmware versions. When the delta equals 3764, the code adds 1376 to the base and branches directly (bx) to the unexported firmware function vMSendSms.

The emulator accommodates this by intentionally spacing out its trap slot allocator so the resulting virtual pointers satisfy the expected offsets:

python
# emu/runtime.py: Constructing non-contiguous trap layouts
OLD_SMS_DIFF, OLD_SMS_ADD = 3764, 1376

def _old_gamelib_sms(self, addr):
    m = self.mach
    pay = m.new_trap("BILLING_GetPayNumByAppId@v3", ...)
    # Force slot allocator forward by 3764 bytes
    m.next_trap += self.OLD_SMS_DIFF // 4 - 1
    remain = m.new_trap("BILLING_GetRemainDay@v3", ...)
    # Advance by 1376 bytes to position unexported vMSendSms
    m.next_trap += self.OLD_SMS_ADD // 4 - 1
    m.new_trap("vMSendSms@v3", send_sms)

    m.w32(addr + 0x240, pay)
    m.w32(addr + 0x244, remain)

4. Runtime State, Clocks, and Lifecycle Traps (GodWar)

Investigation of GodWar resolved four cascading issues:

  1. Expanding the Observation Surface: Early attempts appeared to stall without requesting maps because hooks were attached only to DF_GetTResource and DF_GetResourceIDByFileName, while scenes were fetched via DF_GetStreamTResource.
  2. Decompressed Stream Cache Invalidation: GetStreamDataFormRes initially cached decompression pointers. However, memory ownership belongs to the guest module, which calls free() after consuming data. Once the heap allocator reallocated that buffer, the host cache continued returning the stale pointer. On reload, guangmingshen_jineng.actor interpreted garbage memory as an image count of 3,094, hanging the parser. The correct behavior: decompression buffers must be freshly allocated on every request and left for the guest to free.
  3. String Length Encoding Discrepancies: DF_ReadString uses a 1-byte (u8) length prefix in scene files (SCE2), while DF_ReadString2 uses a 4-byte (u32) prefix in script files (XSE0). Reading DF_ReadString2 as u8 left three NUL bytes in the stream, desynchronizing the script bytecode parser.
  4. Virtual Clock Busy-Wait Detection: Script VM wait loops poll VMGetTickCount():
    asm
    0x11bcc: bl  #0xdeaa         ; Query VMGetTickCount()
    0x11c48: ldr r2, [r0, #0x18]   ; Target wake tick
    0x11c4a: cmp r2, r6
    0x11c4c: bgt #0x11bcc        ; Spin if current tick < target
    In an emulator where time steps between frame boundaries, the tick remains static within a single frame, causing hundreds of thousands of spin iterations.
    Solution: Spin Detection—When the same trap slot is struck repeatedly with no intervening host operations, the runtime recognizes a busy-wait and increments the virtual tick counter.

5. Graphics Pipelines and Codec Anomalies

(1) Firmware Blit Disassembly: Stride Alignment and Swapped Arguments

Disassembling vMDrawImageWithClipEx (@0x530b3f) confirmed two non-standard behaviors:

  • Stride Padding: Scanlines are padded to multiples of 4 pixels: stride=w+((4−w) & 3)\text{stride} = w + ((4 - w) \ \& \ 3) Missing this padding makes images whose width is not a multiple of 4 shear row by row across the display buffer.
  • Swapped Coordinates: The arguments are ordered as: (dst, src, srcX, srcY, w, h, dstX, dstY)
    Source coordinates are passed in slots 3/4, while destination coordinates are passed in slots 7/8.

(2) The 3-Byte Offset Bug in Type-1 GIF Decoding

Type-1 resources wrap GIF data in a custom container:

text
┌──────────────┬────────────────────────┬─────────────────────────┐
│ 8-Byte Head  │ Big-Endian Palette     │ 0x21 0xF9 0x04 GCE      │ ...
└──────────────┴────────────────────────┴─────────────────────────┘

The original parser advanced the stream offset by an extra 3 bytes (offset += 3) after reading the palette. This skipped over the 0x21 0xF9 0x04 Graphic Control Extension containing the transparency index. The subsequent 0x2C image descriptor parsed without error, producing opaque black boxes around sprites. Removing this offset restored transparency across all 544 GIF assets.

(3) Dynamic Resolution Detection

Six modules in the corpus use non-standard resolutions: WaiWai Cat runs at 176×220, Doodle Jump at 240×352, and both editions of My Own Swordsman, Three Kingdoms and Three Kingdoms! at 240×320. These games do not query host display metrics; they write directly to their internal frame widths. The emulator captures full-screen clear (VMFillRectEx) and clipping boundaries (SetClip) during the first 4 frames to dynamically adjust framebuffer dimensions.


6. Virtual File System (VFS) and Persistence Pitfalls

  • ASCII Mode Strings: Path strings use UCS-2, but the mode parameter uses single-byte ASCII ("rb", "w+"). Single-character modes happen to work either way ("r" is 72 00 in both), but parsing "w+" as UCS-2 yields U+2B77, an unrecognized mode that falls back to read-only, so games like Gear of War could never save their configuration.
  • Error Return Codes: File open routines return -1 on failure, not 0. Because 0 is a valid handle, returning 0 makes the guest believe the file exists, read zero bytes, and then parse the null pointer returned by malloc(0).
  • Partial File Updates: GodWar updates its 163-byte GodWarGameRecord save by overwriting only the first 20 bytes (while loading reads 159). If the host’s Storage_Date opens an existing save with truncating "wb", the file shrinks to 20 bytes and can no longer be loaded. Existing saves must be opened with non-truncating "r+b", overwriting only the first n bytes.
  • Storage_Date Polarity: Disassembly confirms argument 4 is isLoad: passing 0 serializes memory to disk, while passing 1 reads data into memory.

7. Offline Authentication and SMS State Machines

CoolBar modules attempt online activation against wmix.kvbar.com at boot. Forcing BILLING_GetPayNumByAppId to return 0 (no billing configured) directs the internal state machine straight into the local offline execution branch, requiring zero binary patches.

For billing prompts triggered at the end of trial levels (for example the SMS purchase after WaiWai Cat’s demo), the emulator simulates the asynchronous SMS callback:

python
# Emulating asynchronous SMS dispatch callback
# Calling back with 0 means "sent successfully"; the module then sets its paid flag and continues
self.defer(callback_ptr, (0,), "smsResult")

Content originally unlocked by paid SMS stays playable with no network at all.


VI. Architectural Evolution: Reference Python to Production Rust

1. Eliminating Pipeline Bottlenecks

Profiling the initial Python prototype revealed that 99.7% of frame time was consumed converting RGB565 to RGB888 and encoding PNGs via PIL, while Unicorn CPU simulation accounted for under 0.3%.

text
Early Prototype Pipeline:
Unicorn Memory ──► Python RGB888 Conversion (47.5 ms) ──► PIL PNG Encoding (53.2 ms) ──► UI
Frame Time: ~100 ms (approx. 10 FPS)

Optimized Architecture:
Unicorn Memory ──► One mem_read of raw RGB565 (0.01 ms) ──► Frontend converts and displays
Core-side display cost: from ~100 ms down to negligible

The core no longer encodes images at all: it hands the raw RGB565 frame over the C ABI, and each frontend (SwiftUI, Qt, Android, browser) does its own color conversion; the macOS app additionally offers optional Scale2x/Scale3x pixel upscaling.

2. Native Thumb Stubs for High-Frequency APIs

Profiling identified that VMGetCurrMainScreenImage (which returns the framebuffer pointer) is called from a few hundred to over 1,500 times per frame in GodWar depending on the scene, at peak accounting for three quarters of all host exits.

To eliminate this overhead, the runtime generates native Thumb stubs directly in unhooked memory:

asm
ldr r0, [pc, #0]    ; Load constant literal stored below
bx  lr              ; Return immediately to guest
.word <framebuffer>  ; value that never changes at runtime

Unicorn executes these routines internally without exiting to Python or Rust; this alone removes roughly three quarters of GodWar’s host round trips per frame.

3. Dual-Track Differential Verification

To ensure that the Rust implementation (rust/emucore) introduced no regressions, the project built an automated differential testing pipeline that compares both implementations against recorded per-frame baselines:

python
# tools/baseline.py: three fingerprints recorded per frame (excerpt)
for i in range(frames):
    rt.mach.call_log.clear()
    apply_input(rt, i)
    rt.frame()
    names = "\n".join(c[0] for c in rt.mach.call_log)
    rows.append((i,
                 zlib.crc32(rt.fb.raw565()) & 0xFFFFFFFF,   # 1. framebuffer CRC32: "drew it wrong"
                 zlib.crc32(names.encode()) & 0xFFFFFFFF,   # 2. CRC32 of call-name sequence: "took a wrong path"
                 len(rt.mach.call_log)))                    # 3. number of host calls this frame
  • Pass Criterion: Framebuffer CRC32, call-name-sequence CRC32 and call count all match for 300 consecutive frames (the call fingerprint hashes names only, not arguments, because heap addresses may legitimately differ between implementations);
  • Result: Verified with tools/rsdiff.sh, the Rust core matches the recorded baselines (generated by the Python reference implementation) frame for frame on all 35/35 modules.

VII. Multi-Platform Deployment and Tooling

1. Cross-Platform Runtime Layout

text
┌────────────────────────────────────────────────────────────────────────┐
│                        nieche-core (Rust Core)                         │
└──────────────────────────────────┬─────────────────────────────────────┘
                                   │
      ┌────────────────────────────┼────────────────────────────┐
      ▼ (Subprocess Pipe IPC)      ▼ (Chaquopy + ctypes)        ▼ (Qt 6)
┌──────────────────────┐    ┌──────────────────────┐    ┌──────────────────────┐
│  macOS (SwiftUI)     │    │  Android (Kotlin)    │    │  Windows (Qt 6)      │
│  NiecheEmu.app       │    │  NiecheEmu.apk       │    │  NiecheEmu.exe       │
│  - Isolated Engine   │    │  - Touch Mapping     │    │  - Emulation Thread  │
│  - Remote Catalogs   │    │  - Phone Keypad      │    │  - Crash Logging     │
│  - Scale2x/3x        │    │  - Memory Editor     │    │                      │
└──────────────────────┘    └──────────────────────┘    └──────────────────────┘
  • macOS: Built with SwiftUI. The emulation engine runs in an isolated worker process communicating via binary stdio pipes, keeping UI threads responsive. Supports remote package catalogs via index.json.
  • Android: A Kotlin shell embeds a thin Python glue layer via Chaquopy, which calls the cross-compiled libnieche.so through ctypes. An on-screen feature-phone keypad sits below the screen, and a memory editor is built in. Landscape games are played by turning the phone sideways (screen rotation is not supported yet).
  • Windows: The released app is built with Qt 6 in CI; the repository also contains an experimental native Win32 frontend (rust/emuwin).
  • Web: tools/run_web.py starts a local HTTP server; the browser long-polls for frames and POSTs input. It is mainly a development tool.

2. 12×12 Dot-Matrix Chinese Typography

The handset’s original font sits in a private container inside the firmware’s language pack and has not been recovered yet. So that story text still displays, the emulator ships a replacement font, font12.cbef, whose encoding, indexing and metrics match the original (all 8,836 GB2312 code points; 12×12 for CJK, 6×12 half-width for ASCII):

  • The first version used tools/fontgen/mkfont.swift to shrink system fonts to 12 px via CoreText and binarize them, but strokes ran together and looked blurry;
  • It now uses the open-source Fusion Pixel 12px bitmap font (OFL), converted with tools/fontgen/bdf2cbef.py. Of the 176 characters Fusion Pixel lacks, 31 full-width math symbols were hand-drawn and 145 rare characters were taken from GNU Unifont and reduced to 12×12;
  • VMGetFontWidth(isFullWidth) reports half- or full-width advances strictly according to its argument; ignoring the argument makes all CJK characters pile on top of each other, which looks like a broken font.

3. Memory Cheat Engine

The core provides a memory search interface: search the module’s RW data segment and heap for a value, filter again after it changes, then edit or lock the few remaining candidates (locked values are rewritten every frame). The Android memory editor also has game-specific options: in Three Kingdoms: Red Cliff (三国志·赤壁传) you can set officers’ levels directly and lock weapon durability, and in Lost in Island (孤岛) you can edit survival points directly.


VIII. Full Corpus Regression and Status

The regression script python3 tools/batch.py confirms that all 35 modules reach their main loops. The table below lists 15 of them. The Non-Black Pixels column is the number of non-black pixels after the Rust core runs 300 frames with no input (the denominator is the module’s screen size; at that point most games sit on a title or menu screen and GodWar on its opening text page, so the number only shows that the screen renders). Status was confirmed by playing into actual gameplay by hand.

ModuleNon-Black Pixels (300 frames, no input)StatusKey Engineering Milestone
众神之战.CBE (GodWar)1,389 / 96,000Fully PlayableTouch navigation, spin-wait escape, stream cache fix, partial save updates
魔塔.CBE (Magic Tower)96,000 / 96,000Fully PlayableDot-matrix narrative text layout, 12-key bitmask keypad mapping
战争机器.CBE (Gear of War)96,000 / 96,000Fully PlayableMulti-package container (sub-packages loaded by name), single-byte ASCII "w+" mode
皇牌空战.CBE (Ace Air Combat)93,407 / 96,000Fully PlayableFixed allocations of size garbage caused by guessing one- vs. two-argument calls from stale registers
武林外传(新品).CBE (My Own Swordsman, New Edition)47,103 / 76,800Fully PlayableReconstructed 35 legacy syscalls, Syscall 2001 writebacks, SMS address arithmetic
武林外传V10.CBE (My Own Swordsman V10)47,205 / 76,800Fully Playable240×320 dynamic resolution adoption, legacy game-library table layout
歪歪猫发条城历险记.CBE (WaiWai Cat)23,222 / 38,720Fully Playable176×220 QCIF adaptation, empty placeholder platform files, key events via logic callback, async SMS callback
涂鸦跳跃.CBE (Doodle Jump)84,480 / 84,480Fully Playable240×352 resolution recognition, key and touch input
恶魔城.CBE (Demon Castle)96,000 / 96,000Fully PlayableMulti-package decompression, layered tilemap Blit blending
僵尸先生.CBE (Mr. Zombie)95,760 / 96,000Fully PlayableSub-package objects, GBK resource names, touch events, deferred screen switching
三国志!.CBE (Three Kingdoms!)71,019 / 76,800Fully PlayableLauncher and embedded game separated, live r9 preservation, memory pool hooks
极品飞车2012.CBE (racing game; its title screen reads The Need for Speed 2012)96,000 / 96,000Fully PlayablePseudo-3D road rendering, high-frequency framebuffer updates
愤怒的小鸟.CBE (Bird’s Anger)96,000 / 96,000Fully PlayableLandscape game in a portrait buffer, 270° rotation with inverse touch transform
捕鱼猎人.CBE (Fish Hunter)96,000 / 96,000Fully PlayableType-1 GIF transparency decoding
AppStore.CBE88,820 / 96,000NavigableAddress 0 zero-fill defense, -1 on failed file open

Known Limitations

  1. Network-Dependent Titles: Pure-online MMORPG modules (such as Jianghu OL) finish unpacking local resources and initializing the UI, then stop at “Fetching version info: contacting server”. The servers are long gone, so they cannot be played without a server emulator;
  2. Original Font: The current 12×12 font is a replacement (Fusion Pixel 12px) with matching encoding and metrics; reverse-engineering the firmware’s private font container is not finished.

IX. Conclusion

By combining automated mining of the firmware’s DWARF symbols, a hardware-level ARM trampoline that resolves the two-SDK conflict, reconstruction of the legacy numeric system calls from the binaries, and dual-track differential testing from Python to Rust, nieche-emu achieves high-fidelity emulation of the CoolBar .CBE application ecosystem without any official technical documentation.

The complete toolchain and platform frontends are open source, including DWARF extractors, resource unpackers, disassembly scripts, and multi-platform clients, all available under the nieche-cbe-emu GitHub organization.