原生鸿蒙 GBC 模拟器开发实录:用 Anime4K 把 160×144 画面实时放大到 1080P

原生鸿蒙 GBC 模拟器开发实录:用 Anime4K 把 160×144 画面实时放大到 1080P

导语:160×144 像素,4.19 MHz 主频,1998 年的一台彩色掌机。二十多年后,我用 ArkTS + C++ 把它做成了鸿蒙原生应用,还给它加上了 Anime4K AI 超分滤镜和正版 ROM 校验。
摘要 :本文完整复盘一款 HarmonyOS 原生 GB / GBC 模拟器 开发主要过程。第一章讲清在鸿蒙上开发模拟器的整体思路与工程搭建;第二章是全文重点,逐一拆解五个重难点------SameBoy 核心移植、NAPI + ArkTS 渲染架构、OpenGL ES 纹理绘制、Anime4K 超分移植、游戏速度调整。

关键词 :HarmonyOS 鸿蒙 GBC模拟器 SameBoy NAPI XComponent OpenGL ES Anime4K GLSL 超分辨率 原生开发 应用上架

项目 内容
应用名称 GBC模拟器
应用市场 点击前往华为应用市场

文章目录

  • [原生鸿蒙 GBC 模拟器开发实录:用 Anime4K 把 160×144 画面实时放大到 1080P](#原生鸿蒙 GBC 模拟器开发实录:用 Anime4K 把 160×144 画面实时放大到 1080P)
    • 一、如何在原生鸿蒙开发GBC模拟器
      • [1.1 先想清楚:模拟一台 GBC 到底要复刻什么](#1.1 先想清楚:模拟一台 GBC 到底要复刻什么)
      • [1.2 技术选型:为什么是「ArkTS + NAPI + C++ 核心」](#1.2 技术选型:为什么是「ArkTS + NAPI + C++ 核心」)
      • [1.3 整体架构](#1.3 整体架构)
      • [1.4 工程搭建:DevEco Studio + Native C++ 模块](#1.4 工程搭建:DevEco Studio + Native C++ 模块)
      • [1.5 最小闭环:从 `GB_load_rom` 到第一帧画面](#1.5 最小闭环:从 GB_load_rom 到第一帧画面)
    • 二、GBC模拟器开发的重难点
      • [2.1 难点一:SameBoy 核心移植](#2.1 难点一:SameBoy 核心移植)
      • [2.2 难点二:NAPI + ArkTS 的高效图形渲染架构](#2.2 难点二:NAPI + ArkTS 的高效图形渲染架构)
      • [2.3 难点三:OpenGL ES 纹理绘制](#2.3 难点三:OpenGL ES 纹理绘制)
      • [2.4 难点四:Anime4K 超分移植](#2.4 难点四:Anime4K 超分移植)
        • [Anime4K 是什么](#Anime4K 是什么)
        • [移植路径:GLSL → GLSL ES](#移植路径:GLSL → GLSL ES)
        • 滤镜链的调度
      • [2.5 难点五:游戏速度调整](#2.5 难点五:游戏速度调整)
      • [2.6 五个难点的经验总结](#2.6 五个难点的经验总结)
    • 三、GBC模拟器需要遵循哪些相关标准、规则以及法律
      • [3.1 开源许可证合规:这是最容易被忽略的一条](#3.1 开源许可证合规:这是最容易被忽略的一条)
      • [3.2 模拟器的法律地位与 ROM 版权](#3.2 模拟器的法律地位与 ROM 版权)
        • 模拟器软件本身
        • [但 2024 年的 Yuzu 案划出了新的红线](#但 2024 年的 Yuzu 案划出了新的红线)
        • [ROM 的版权归属](#ROM 的版权归属)
      • [3.3 中国大陆上架必须满足的合规要求](#3.3 中国大陆上架必须满足的合规要求)
      • [3.4 我的落地做法:把合规做进产品里](#3.4 我的落地做法:把合规做进产品里)
    • 四、这款GBC模拟器的特色功能
      • [4.1 Anime4K AI 超分](#4.1 Anime4K AI 超分)
      • [4.2 正版 ROM 校验与 DAT 数据库](#4.2 正版 ROM 校验与 DAT 数据库)
      • [4.3 18 种滤镜与 1 ~ 16 倍点对点缩放](#4.3 18 种滤镜与 1 ~ 16 倍点对点缩放)
      • [4.4 画面、音频动态同步加速](#4.4 画面、音频动态同步加速)
      • [4.5 即时存档与批量导出](#4.5 即时存档与批量导出)
      • [4.6 自研测试 ROM《宇宙探索》](#4.6 自研测试 ROM《宇宙探索》)
      • [4.7 隐私与无广告](#4.7 隐私与无广告)
    • 五、如何上架鸿蒙应用商店
      • [5.1 上架前的三件事](#5.1 上架前的三件事)
      • [5.2 签名与打包](#5.2 签名与打包)
      • [5.3 在 AppGallery Connect 提交](#5.3 在 AppGallery Connect 提交)
      • [5.4 审核容易被驳回的几类问题](#5.4 审核容易被驳回的几类问题)
      • [5.5 我的上架结果](#5.5 我的上架结果)
      • [5.6 上架检查清单](#5.6 上架检查清单)
    • 下载与参考资料

一、如何在原生鸿蒙开发GBC模拟器

1.1 先想清楚:模拟一台 GBC 到底要复刻什么

GBC 的硬件参数如下:

项目 规格
处理器 Sharp LR35902(基于 Zilog Z80 定制)
主频 4.194304 MHz,双速模式下 8.388608 MHz
屏幕分辨率 160 × 144 像素
发售时间 1998 年 10 月 21 日(日本),定价 8,900 日元

但"能跑起来"和"跑得对"是两回事。要用软件扮演这台机器,必须让四个子系统在正确的时刻做正确的事:

子系统 职责 做不对会怎样
CPU(SM83) 逐条解释执行指令,节奏对齐 4.19 MHz 游戏逻辑错乱、卡死
PPU(图形) 逐扫描线生成 160×144 画面 花屏、精灵错位、滚动撕裂
APU(音频) 4 声道合成 爆音、变调
卡带控制器(MBC) 存储体切换、存档读写 大容量卡带读不出来、存档丢失

所以第一件事不是写代码,而是决定"自己写核心"还是"移植成熟核心"。 我选了后者,一直成熟的sameboy核心,理由在第二章 2.1 节展开。

1.2 技术选型:为什么是「ArkTS + NAPI + C++ 核心」

鸿蒙应用层是 ArkTS(声明式 UI),但模拟器核心是性能敏感的 C 代码。两者之间需要一个桥,这就是 NAPI(Native API)。

层次 选型 职责
界面层 ArkTS / ArkUI ROM 库、设置页、虚拟按键、菜单
桥接层 NAPI(C++) ArkTS 与 C++ 互相调用、帧数据传递
核心层 C / C++(SameBoy) CPU / PPU / APU / MBC 全部模拟逻辑
输出层 XComponent + OpenGL ES / EGL 纹理上传、滤镜后处理、上屏
音频输出 系统音频渲染管线 PCM 实时播放

为什么不让 ArkTS 直接画像素?因为 160×144 的画面每帧要处理 23040 个像素,逐像素走 JS 引擎的桥,经过实测,性能较弱,模拟起来非常卡顿。

1.3 整体架构

#mermaid-svg-eKM2Nx7HFB3lCL5G{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eKM2Nx7HFB3lCL5G .error-icon{fill:#552222;}#mermaid-svg-eKM2Nx7HFB3lCL5G .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eKM2Nx7HFB3lCL5G .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .marker.cross{stroke:#333333;}#mermaid-svg-eKM2Nx7HFB3lCL5G svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eKM2Nx7HFB3lCL5G p{margin:0;}#mermaid-svg-eKM2Nx7HFB3lCL5G .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster-label text{fill:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster-label span{color:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster-label span p{background-color:transparent;}#mermaid-svg-eKM2Nx7HFB3lCL5G .label text,#mermaid-svg-eKM2Nx7HFB3lCL5G span{fill:#333;color:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .node rect,#mermaid-svg-eKM2Nx7HFB3lCL5G .node circle,#mermaid-svg-eKM2Nx7HFB3lCL5G .node ellipse,#mermaid-svg-eKM2Nx7HFB3lCL5G .node polygon,#mermaid-svg-eKM2Nx7HFB3lCL5G .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .rough-node .label text,#mermaid-svg-eKM2Nx7HFB3lCL5G .node .label text,#mermaid-svg-eKM2Nx7HFB3lCL5G .image-shape .label,#mermaid-svg-eKM2Nx7HFB3lCL5G .icon-shape .label{text-anchor:middle;}#mermaid-svg-eKM2Nx7HFB3lCL5G .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .rough-node .label,#mermaid-svg-eKM2Nx7HFB3lCL5G .node .label,#mermaid-svg-eKM2Nx7HFB3lCL5G .image-shape .label,#mermaid-svg-eKM2Nx7HFB3lCL5G .icon-shape .label{text-align:center;}#mermaid-svg-eKM2Nx7HFB3lCL5G .node.clickable{cursor:pointer;}#mermaid-svg-eKM2Nx7HFB3lCL5G .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .arrowheadPath{fill:#333333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eKM2Nx7HFB3lCL5G .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eKM2Nx7HFB3lCL5G .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eKM2Nx7HFB3lCL5G .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster text{fill:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G .cluster span{color:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eKM2Nx7HFB3lCL5G .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eKM2Nx7HFB3lCL5G rect.text{fill:none;stroke-width:0;}#mermaid-svg-eKM2Nx7HFB3lCL5G .icon-shape,#mermaid-svg-eKM2Nx7HFB3lCL5G .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eKM2Nx7HFB3lCL5G .icon-shape p,#mermaid-svg-eKM2Nx7HFB3lCL5G .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eKM2Nx7HFB3lCL5G .icon-shape .label rect,#mermaid-svg-eKM2Nx7HFB3lCL5G .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eKM2Nx7HFB3lCL5G .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eKM2Nx7HFB3lCL5G .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eKM2Nx7HFB3lCL5G :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出层
核心层 · C / C++ (SameBoy)
桥接层 · NAPI
界面层 · ArkTS / ArkUI
ROM 库与模拟器设置
虚拟按键与蓝牙手柄
缩放 / 滤镜 / 声音菜单
ArkTS 调用 C++
帧缓冲与音频数据回传
sm83_cpu 指令解释
display 逐扫描线渲染
apu 四声道混音
mbc / memory 卡带与存储
ROM 指纹校验
XComponent 原生 Surface
EGL 上下文 + GL 纹理
18 种滤镜后处理链
系统音频渲染管线

1.4 工程搭建:DevEco Studio + Native C++ 模块

在 DevEco Studio 里新建工程时勾选 Native C++ 模板,会得到一个 ArkTS 主模块加一个 src/main/cpp 的 Native 目录。桥接层对 ArkTS 只需暴露少量接口,在 index.d.ts 里声明即可:

typescript 复制代码
// index.d.ts ------ 桥接层暴露给 ArkTS 的接口(示例)
export const loadRom: (buffer: ArrayBuffer) => boolean;
export const runFrame: () => void;
export const setInput: (mask: number) => void;

把 SameBoy 的 Core/ 目录整体拷进 cpp/sameboy/,在 CMakeLists.txt 里编译成静态库即可------SameBoy 核心是纯 C、零外部依赖,这一步几乎没有编译摩擦,这也是选它的重要原因。

text 复制代码
entry/src/main/cpp/
├─ CMakeLists.txt
├─ napi_init.cpp          # NAPI 模块注册与接口导出
├─ gbc_wrapper.cpp        # 模拟器生命周期、线程、缓冲区管理
├─ render/                # EGL 初始化、纹理上传、滤镜链
│   ├─ egl_core.cpp
│   ├─ shader_pipeline.cpp
│   └─ shaders/           # 18 种滤镜的 GLSL ES 源码
└─ sameboy/Core/          # SameBoy 核心(未修改,保持原样便于升级)

1.5 最小闭环:从 GB_load_rom 到第一帧画面

先把"能出一帧画面"跑通,剩下的都是优化。 这是整个项目的第一条主线:

cpp 复制代码
// 载入 ROM → 推进一帧 → 取回画面,这三步就是模拟器的骨架
GB_load_rom_from_buffer(&gb, rom_data, rom_size);
GB_run_frame(&gb);
uint32_t *pixels = GB_get_pixels_output(&gb);

就这么几行,画面就出来了。注意 GB_run_frame 的返回值 ------它返回"距上一帧实际经过的纳秒数",这个数字是后面做变速 和音画同步的基础,第二章 2.5 节会用到。


二、GBC模拟器开发的重难点

这一章是全文重点。五个难点不是并列关系,而是层层依赖:
#mermaid-svg-oLLOG0o8zXEGfTll{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oLLOG0o8zXEGfTll .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oLLOG0o8zXEGfTll .error-icon{fill:#552222;}#mermaid-svg-oLLOG0o8zXEGfTll .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oLLOG0o8zXEGfTll .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oLLOG0o8zXEGfTll .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oLLOG0o8zXEGfTll .marker.cross{stroke:#333333;}#mermaid-svg-oLLOG0o8zXEGfTll svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oLLOG0o8zXEGfTll p{margin:0;}#mermaid-svg-oLLOG0o8zXEGfTll .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oLLOG0o8zXEGfTll .cluster-label text{fill:#333;}#mermaid-svg-oLLOG0o8zXEGfTll .cluster-label span{color:#333;}#mermaid-svg-oLLOG0o8zXEGfTll .cluster-label span p{background-color:transparent;}#mermaid-svg-oLLOG0o8zXEGfTll .label text,#mermaid-svg-oLLOG0o8zXEGfTll span{fill:#333;color:#333;}#mermaid-svg-oLLOG0o8zXEGfTll .node rect,#mermaid-svg-oLLOG0o8zXEGfTll .node circle,#mermaid-svg-oLLOG0o8zXEGfTll .node ellipse,#mermaid-svg-oLLOG0o8zXEGfTll .node polygon,#mermaid-svg-oLLOG0o8zXEGfTll .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oLLOG0o8zXEGfTll .rough-node .label text,#mermaid-svg-oLLOG0o8zXEGfTll .node .label text,#mermaid-svg-oLLOG0o8zXEGfTll .image-shape .label,#mermaid-svg-oLLOG0o8zXEGfTll .icon-shape .label{text-anchor:middle;}#mermaid-svg-oLLOG0o8zXEGfTll .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oLLOG0o8zXEGfTll .rough-node .label,#mermaid-svg-oLLOG0o8zXEGfTll .node .label,#mermaid-svg-oLLOG0o8zXEGfTll .image-shape .label,#mermaid-svg-oLLOG0o8zXEGfTll .icon-shape .label{text-align:center;}#mermaid-svg-oLLOG0o8zXEGfTll .node.clickable{cursor:pointer;}#mermaid-svg-oLLOG0o8zXEGfTll .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oLLOG0o8zXEGfTll .arrowheadPath{fill:#333333;}#mermaid-svg-oLLOG0o8zXEGfTll .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oLLOG0o8zXEGfTll .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oLLOG0o8zXEGfTll .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oLLOG0o8zXEGfTll .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oLLOG0o8zXEGfTll .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oLLOG0o8zXEGfTll .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oLLOG0o8zXEGfTll .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oLLOG0o8zXEGfTll .cluster text{fill:#333;}#mermaid-svg-oLLOG0o8zXEGfTll .cluster span{color:#333;}#mermaid-svg-oLLOG0o8zXEGfTll div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oLLOG0o8zXEGfTll .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oLLOG0o8zXEGfTll rect.text{fill:none;stroke-width:0;}#mermaid-svg-oLLOG0o8zXEGfTll .icon-shape,#mermaid-svg-oLLOG0o8zXEGfTll .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oLLOG0o8zXEGfTll .icon-shape p,#mermaid-svg-oLLOG0o8zXEGfTll .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oLLOG0o8zXEGfTll .icon-shape .label rect,#mermaid-svg-oLLOG0o8zXEGfTll .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oLLOG0o8zXEGfTll .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oLLOG0o8zXEGfTll .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oLLOG0o8zXEGfTll :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① SameBoy 核心移植

先让画面出来
② NAPI + ArkTS 架构

让数据高效过桥
③ OpenGL ES 纹理绘制

让画面高效上屏
④ Anime4K 超分移植

让画面变清晰
⑤ 游戏速度调整

让时间可控

2.1 难点一:SameBoy 核心移植

为什么选 SameBoy
候选 语言 许可 取舍
SameBoy 纯 C Expat(MIT 系) Copyright © 2015-2026 Lior Halphon 精度极高、零外部依赖、核心与前端彻底解耦
Gambatte C++ GPL 精度好,但 GPL 传染性对商业分发不友好
Peanut-GB C MIT 体积极小,但精度做了取舍

决定性因素是架构 :SameBoy 本身就是"核心库 + 多前端"的设计(Core/ 纯 C 无依赖,上层分别有 Cocoa、SDL、libretro 三套前端)。这意味着它的核心天生就是为被别的前端调用而写的------我要做的只是再写一个鸿蒙前端。

Core/ 目录共 44 个文件,模块划分非常干净:

文件 职责
sm83_cpu.c LR35902 指令解释执行
display.c PPU:扫描线渲染、精灵、窗口层
apu.c 4 声道音频合成
mbc.c / memory.c 卡带控制器与存储映射
save_state.c / rewind.c 即时存档与倒退
timing.c 时序调度
gb.c / gb.h 对外 API 全部在这里
核心 API 的分层

移植的本质就是"找到该调用的那几个函数"。SameBoy 对外暴露上百个接口,实际项目里高频使用的只有这些:

用途 接口
生命周期 GB_init()、GB_free()、GB_reset()
载入 ROM GB_load_rom()、GB_load_rom_from_buffer()
推进运行 GB_run()、GB_run_frame()
取画面 GB_get_pixels_output()
输入 GB_set_input_callback()
存档 GB_save_battery()、GB_load_battery()、GB_get_battery_dirty()、GB_clear_battery_dirty()
变速 GB_set_clock_multiplier()、GB_set_turbo_mode()、GB_set_turbo_cap()
性能 GB_set_rendering_disabled()
ROM 信息 GB_get_rom_title()、GB_get_rom_crc32()

注意:GB_update_clock_rate() 虽然也在头文件里,但它被 #ifdef GB_INTERNAL 包裹,属于核心内部接口,前端不应该调用 。变速要用的公开接口是 GB_set_clock_multiplier()。

移植时真正要改的三件事

核心代码一行没动,我要做的是把它的"外设"接到鸿蒙上:

  1. 输入 :实现 GB_set_input_callback,把 ArkTS 传来的按键掩码返回给核心。核心会在每一帧的合适时机主动来问,不要自己去"推"。
  2. 画面 :不去订阅逐行回调,而是每帧结束后调用 GB_get_pixels_output() 取整块缓冲。逐行回调(GB_set_lcd_line_callback)留给需要做研究级调试的场景。
  3. 存档 :核心不会自己写文件。正确姿势是轮询 GB_get_battery_dirty(),为真时调用 GB_save_battery_to_buffer() 落盘,然后 GB_clear_battery_dirty() 复位标志。
cpp 复制代码
// 存档落盘:用"脏标志"驱动,而不是用定时器硬写
if (GB_get_battery_dirty(&gb)) {
    GB_save_battery_to_buffer(&gb, save_buffer, GB_save_battery_size(&gb));
    GB_clear_battery_dirty(&gb);
}

2.2 难点二:NAPI + ArkTS 的高效图形渲染架构

NAPI 的职责边界要划清楚

NAPI 只做两件事,其余一律不碰:

方向 内容 频率
ArkTS → C++ 启动、载入 ROM、改设置、按键状态 低频(按键除外)
C++ → ArkTS 帧缓冲、音频数据 每帧一次

按键是唯一的例外 :它虽然是 ArkTS → C++,但频率可能到每秒几十次。所以按键不要用"每次事件调一次 NAPI"的方式传,而是让 ArkTS 只更新一个原子变量,C++ 侧在输入回调里直接读。

最大的性能陷阱:每帧分配内存

这是整个项目里最容易被忽略、但影响最大的一处。

反面写法(我最初就是这么写的):

cpp 复制代码
// 错误示范:每帧新建 ArrayBuffer,再整块拷贝一次
napi_create_arraybuffer(env, 160 * 144 * 4, &data, &buffer);
memcpy(data, GB_get_pixels_output(&gb), 160 * 144 * 4);

一秒钟 60 帧,就是每秒 60 次 90 KB 的内存分配 + 60 次拷贝。结果是GC 压力陡增、内存碎片累积、帧率周期性抖动。

正确做法是复用同一块缓冲区:

cpp 复制代码
// 正确做法:缓冲区在初始化阶段创建一次并长期持有
static uint8_t *g_frame_buffer_data;   // 只绑定一次

// 每帧只做"回填",零分配
memcpy(g_frame_buffer_data, GB_get_pixels_output(&gb), 160 * 144 * 4);

再把 napi_create_external 与 napi_create_reference 配合使用,让 ArkTS 侧"看到"的始终是同一个 ArrayBuffer,这样既能避免分配,也能避免 ArkTS 侧产生新的对象引用。

线程模型

模拟线程与 UI 线程必须分开,但要避免锁竞争:

线程 做什么 与另一线程的交互方式
模拟线程(Native) GB_run_frame() 循环推进 读原子变量(按键)、写帧缓冲
UI 线程(ArkTS) 渲染、响应触控 写原子变量、读帧缓冲

共享数据只有两类:按键掩码(单字节)和帧缓冲(一块固定内存)。 都是"一写一读"的简单结构,用原子变量 + 双缓冲就能保证无锁安全,不需要引入互斥锁------在每帧都要跑 60 次的路径上加锁,代价比问题本身还大。

2.3 难点三:OpenGL ES 纹理绘制

为什么用纹理,而不是逐像素画

先看两条路线的开销对比:

方案 每帧工作量 结果
ArkTS Canvas 逐像素 23040 次像素写入,全部经过 JS 引擎 无法维持满帧
GL 纹理上传 + 着色器放大 一次 90 KB 纹理上传 + 一次 Draw Call 占用极低

关键在于:160×144 只有 90 KB,上传这张小纹理几乎不花时间,放大交给 GPU 的采样器。

XComponent → EGL → GL 的链路

鸿蒙侧通过 XComponent 暴露原生渲染表面:ArkTS 用 XComponentController 拿到 surfaceId,Native 侧再用 OH_NativeXComponent_GetXComponentId 与之对应,然后建立 EGL 上下文:

typescript 复制代码
// ArkTS 侧声明渲染组件:libraryname 指向我们的 NAPI 模块
XComponent({ id: 'gbcSurface', type: XComponentType.SURFACE,
             libraryname: 'libgbc.so', controller: this.controller })
  .onLoad(() => { /* Surface 就绪,开始渲染 */ })
  .width('100%').height('100%')

Native 侧的关键三步:

  1. eglGetDisplay() → eglInitialize() 拿到显示连接;
  2. 用 XComponent 提供的 EGLNativeWindowType 创建 EGLSurface;
  3. 每帧 glTexImage2D() 上传 160×144 像素 → glDrawArrays() 绘制铺满视口的两个三角形 → eglSwapBuffers() 上屏。
点对点缩放:整数倍才是清晰的

这是"画面糊不糊"的关键。非整数倍缩放会让像素落在半个像素的位置上,GPU 双线性插值一采样,边缘立刻发虚。

glsl 复制代码
// 最近邻采样:把坐标换算成整数像素位置再取值,避开半像素导致的模糊
ivec2 texel = ivec2(vTexCoord * uTextureSize);
fragColor = texelFetch(uTexture, texel, 0);

texelFetch 按整数坐标取值,不做插值------这正是像素画需要的"硬边"。选好整数倍后,剩余的那几个像素用黑边补上,而不是拉伸铺满。

图  1 ~ 16 倍点对点缩放,满足不同设备的分辨率需求

2.4 难点四:Anime4K 超分移植

Anime4K 是什么

Anime4K 是 Bloc97 开源的一套高质量实时动漫放大与降噪算法 ,采用 MIT 许可。它的设计目标很明确:针对动画/线稿类画面做实时超分。

这恰好和 GBC 的画面特征对上了:

GBC 画面特征 Anime4K 的设计假设 匹配度
大面积纯色块 色块内部应保持平坦 高
明显的黑/深色描边线条 线条需要保留锐利、不能糊 高
像素边缘规整、少噪声 降噪与边缘保持算法收益大 高

这是我把 Anime4K 引入 GBC 模拟器的依据------不是随便找个滤镜套上去,而是画面特征确实匹配。

移植路径:GLSL → GLSL ES

Anime4K 官方提供 GLSL 实现,但它面向桌面 OpenGL(#version 330+)。移植到鸿蒙的 OpenGL ES 3.0,主要差异集中在四处:

差异点 桌面 GLSL GLSL ES 3.0
精度限定 不需要 必须声明 precision highp float;,否则编译失败
片段输出 gl_FragColor 必须声明 out vec4 fragColor;
纹理采样 texture() 多版本 统一为 texture(),但需注意不支持隐式 LOD 的场景
数组与循环 长度可动态 循环边界必须是编译期常量,动态数组长度受限

其中最容易翻车的是最后一条 :Anime4K 的某些 pass 用到了运行期决定的循环次数,直接搬过来在 ES 上编译不过。解决办法是把循环展开成固定次数 ,或把参数做成 #define 宏在编译期确定。

glsl 复制代码
// AI 超分 pass 的核心思路(示意,非 Anime4K 原始 shader)
vec3 c = texture(uTexture, vTexCoord).rgb;                            // 中心像素
vec3 l = texture(uTexture, vTexCoord - vec2(uTexelSize.x, 0.0)).rgb;  // 左邻域
vec3 r = texture(uTexture, vTexCoord + vec2(uTexelSize.x, 0.0)).rgb;  // 右邻域
float gx = abs(luma(r) - luma(l));   // 水平梯度,luma 为 RGB 加权亮度

// 梯度小的方向就是"线条方向",沿它插值才不会跨越线条
vec3 blurred = (l + r + c * 2.0) * 0.25;
fragColor = vec4(mix(blurred, c, clamp(gx, 0.0, 1.0)), 1.0);
滤镜链的调度

18 种滤镜不是 18 段独立的代码,而是一条可配置的后处理链:

档位 滤镜 处理成本 适用场景
零成本 NearestNeighbor 只切采样方式 追求原汁原味像素感
低成本 Linear、Bilinear、LCD、MonoLCD 单次采样 日常游玩
中成本 HQ2x、AAScale2x、AAScale4x、AAOmniScaleLegacy 多邻域采样 想要平滑边缘
高成本 Anime4K 多 pass AI 超分 大屏 + 性能充裕
风格化 CRT、FlatCRT 多 pass 扫描线 怀旧显像管质感

关键设计决策:Anime4K 做成可选开关,而不是默认开启。

理由很实在------AI 超分要吃算力,而用户设备的性能差异极大。把它强开,等于把"低端机卡顿"变成所有用户的默认体验。做成可选项后,用户自己就能做"画质换性能"的取舍,这比开发者替所有人拍板更合理。

图  画面效果:支持 18 种滤镜,含 Anime4K AI 超分

2.5 难点五:游戏速度调整

变速不是"把帧率调高"

最直观的想法是"加速 = 每秒钟多跑几帧"。但这样做会立刻撞上两个问题:

做法 后果
简单提高每秒执行的帧数 音频来不及消费 → 爆音 ;画面丢帧 → 撕裂
只加速画面不加速音频 音画不同步,声音越拖越远

根因 :GBC 的 CPU、PPU、APU 是按同一个时钟同步推进的。你只动其中一环,三者就脱节了。

用 GB_run_frame 的返回值做音画同步

2.1 节提到 GB_run_frame() 返回"距上一帧实际经过的纳秒数"。这个值就是音画同步的关键:

cpp 复制代码
uint64_t elapsed_ns = GB_run_frame(&gb);   // 距上一帧实际经过的纳秒数
// 按真实时间消费 PCM,而不是"每帧固定送 N 个采样点"
apus_consume_by_time(elapsed_ns, speed_multiplier);

核心思路是以音频时钟为主时钟驱动画面,而不是反过来。 因为人对音频断续的敏感度远高于画面轻微丢帧:画面偶尔掉一帧几乎察觉不到,声音一旦断一下就是刺耳的爆音。

动态同步策略

固定倍率还不够。实际运行中,设备的渲染耗时会有波动,所以我做了一套动态同步算法:

情况 表现 处理策略
画面跟不上 帧缓冲还没渲染完,新帧又来了 丢弃旧帧,音频继续走(音频优先)
音频缓冲将耗尽 即将断音 微调帧推进节奏,让音频补上
加速倍率高 撕裂与爆音风险最大 动态调整倍率上限,按设备性能自适应

这套"画面、音频动态同步加速算法"是这款应用里性能调优花时间最多的一块,也是"加速时减少画面撕裂与爆音"这个特性的由来。

2.6 五个难点的经验总结

难点 最容易踩的坑 经验
SameBoy 移植 试图"修改核心"来适配前端 核心一行不改,只实现回调与轮询接口
NAPI 架构 每帧新建 ArrayBuffer 缓冲区复用,零分配是底线
OpenGL ES 用非整数倍缩放铺满屏幕 先算最大整数倍,再居中加黑边
Anime4K 移植 直接搬桌面 GLSL ES 3.0 的精度声明与静态循环边界必须处理
速度调整 只提高帧率 改时钟倍率让 CPU/PPU/APU 同步加速,以音频为主时钟

三、GBC模拟器需要遵循哪些相关标准、规则以及法律

声明 :本节是基于公开资料的整理与个人实践总结,不构成法律意见。具体合规判断请咨询专业律师,或以上架时平台的审核意见为准。

3.1 开源许可证合规:这是最容易被忽略的一条

用了别人的开源核心,就必须遵守它的许可证。这是硬性技术要求,不是道德倡议。

组件 许可证 我必须履行的义务
SameBoy 核心 Expat License (MIT 系) Copyright © 2015-2026 Lior Halphon 保留版权声明与许可证全文;在应用内或关于页声明使用了该组件
Anime4K MIT License Copyright © Bloc97 同上

SameBoy 的 LICENSE 原文写的是 Expat License (常被统称为 MIT),而不是字面上的 "MIT"------这一点差别必须写对,不能想当然。

我的做法:

  • 应用内"关于"页面列出第三方开源组件清单,包含组件名、许可证类型、版权归属;
  • 仓库中保留各组件原始的 LICENSE 文件,未做删改;
  • 核心代码保持原样、不做魔改,这样升级上游版本时不会产生许可证纠纷。

为什么没选 GPL 系核心 :GPL 具有传染性,会要求整个应用以同许可证开源。Expat/MIT 只要求保留声明,对闭源分发的应用友好得多。技术选型阶段就要把许可证看清楚,别等到要上架才发现问题。

3.2 模拟器的法律地位与 ROM 版权

这两件事经常被混为一谈,但界限其实很清楚。

模拟器软件本身

在美国的司法实践中,模拟器软件本身曾多次被认定为合法:

判例 结论
Sega v. Accolade(第九巡回法院,1992) 为互操作性进行的逆向工程属于合理使用
Sony v. Connectix(第九巡回法院,2000) 逆向工程中的中间复制属于合理使用,Virtual Game Station 模拟器本身不侵权
Sony v. Bleem 商业模拟器本身得以继续销售

共性结论是:模拟器是一种技术中立的产品,能够运行他人软件不等于侵权。

但 2024 年的 Yuzu 案划出了新的红线

任天堂诉 Yuzu 案(2024 年,以和解告终)给整个行业提了醒:风险不在"模拟"这件事本身,而在模拟器是否绕过了技术保护措施、是否在为盗版提供便利。

具体来说,以下行为会显著抬高法律风险:

高危行为 为什么危险
内置解密密钥(如主机固件的 prod.keys) 涉嫌规避技术保护措施
内置或引导下载 ROM 直接参与侵权传播
用"内置游戏"作为卖点引流 以他人作品牟利,主观恶意明显
提供绕过正版验证的教程 教唆侵权
ROM 的版权归属

游戏 ROM 是游戏程序的复制件,受《中华人民共和国著作权法》中关于计算机软件的保护。核心结论很简明:

  • 模拟器是工具,ROM 是作品,两者的法律地位完全不同;
  • 复制、传播 ROM 需要权利人的许可,个人备份自己合法拥有的卡带 与从网络下载 ROM是两件性质不同的事;
  • 本站不给"ROM 是否合法"做背书,判断依据是用户是否持有正版卡带或官方授权版本。

3.3 中国大陆上架必须满足的合规要求

依据华为官方文档《应用资质审核要求》,AppGallery 的合规要求在近两年明显收紧,其中 APP 备案是硬门槛:

要求 具体内容 是否强制
APP 核准(备案) 依据《工业和信息化部关于开展移动互联网应用程序备案工作的通知》,APP 主办者应履行核准(备案)手续;未履行备案的,不得从事 APP 互联网信息服务 强制
增值电信业务经营许可证 从事增值电信业务的需提供 按业务类型
软件著作权证书 《计算机软件著作权登记证书》《APP 电子版权证书》或《软件著作权认证证书》 官方原文为"非必选资质",但建议申请以保护知识产权
游戏版号 《网络游戏出版物号(ISBN)核发单》 仅游戏类应用需要
隐私政策 需提供可公开访问的链接,应用内可查 强制

关于版号 :本应用在应用市场的分类是「工具 」,不是「游戏」,因此不涉及 ISBN 版号 。这一点对模拟器类应用很关键------但分类最终由平台审核判定,不要把"我认为它是工具"当作当然结论。

3.4 我的落地做法:把合规做进产品里

法律要求是底线,但更稳妥的做法是把它变成产品的一部分。这是我在这个项目上最坚持的一点:

做法 具体落地
不内置任何游戏 安装包里没有任何 ROM 文件,装完打开是空的 ROM 库
内置正版 ROM 校验 导入时对 ROM 做指纹校验,来源异常或被篡改的文件不予放行
支持 DAT 数据库导入 校验库不写死,可导入社区维护的指纹数据库持续扩充
不提供下载入口 应用内没有 ROM 下载、推荐、外链跳转
不做"私藏分享" 不搞"内置 N 个经典游戏、开箱即用"那一套
开源组件合规 关于页列出 SameBoy / Anime4K 的许可证与版权声明
权限最小化 未申请敏感权限,访问文件走系统安全访问机制
需要坦白的一点

没有任何一款模拟器能真正证明"你拥有这款游戏"。

指纹校验能判断的是:这份文件是不是某个游戏的原始副本、有没有被改动过、是不是已知的正式游戏。但它查不到你抽屉里有没有那张卡带。

所以校验只是第一道门槛,真正的边界在用户自己手里。我能做的是:

  • 不提供盗版内容;
  • 不提供获取盗版内容的便利;
  • 把这句话同时写进应用和写在这里:请使用你合法拥有的游戏备份。

四、这款GBC模拟器的特色功能

4.1 Anime4K AI 超分

把 Anime4K 引入 GBC 模拟器,是这款应用最核心的差异化点。

项目 说明
技术来源 Anime4K(Bloc97,MIT 许可),面向动画线稿的实时超分算法
移植方式 桌面 GLSL → OpenGL ES 3.0 GLSL ES 重写
实际效果 线条边缘更锐利,纯色区域更干净,大屏下观感提升明显
性能定位 可选开关,不强制开启

具体移植细节见 2.4 节。

4.2 正版 ROM 校验与 DAT 数据库

这是我在这个项目上最"较真"的功能。

它是怎么工作的

导入 ROM 时,应用会读取 ROM 的 CRC32 指纹 (对应 SameBoy 的 GB_get_rom_crc32()),与校验库比对,再决定是否放行:

校验结果 应用的处理
校验通过 正常导入,加入 ROM 库
校验不通过(来源异常 / 文件被篡改) 明确提示原因,不予放行
文件损坏或格式不支持 提示具体原因,建议重新导出备份
为什么要做成"可导入 DAT"

如果把校验库写死在安装包里,会立刻遇到两个问题:

  1. 库会过时:新的游戏、新的版本无法覆盖;
  2. 无法扩容:用户手上有更权威的指纹数据也用不上。

所以我把校验库做成可导入的 DAT 文件 ------DAT 是模拟器社区长期使用的指纹数据库格式,里面记录了大量游戏的标准 CRC32 等校验值。用户导入 DAT 后,校验范围立刻扩展到整库,而不是被安装包锁死。

这个设计让"正版校验"从一个宣传点变成了真正可用的机制。

图  ROM 库:支持批量导入导出 ROM 和存档

4.3 18 种滤镜与 1 ~ 16 倍点对点缩放

能力 说明
滤镜数量 18 种,覆盖像素风、智能放大、AI 超分、真机模拟四类
缩放倍率 1、2、4、6、8、10、12、14、16 倍,全部为整数倍
缩放意义 整数倍保证像素边缘锐利,不产生半像素模糊与摩尔纹
大屏适配 16 倍缩放覆盖高分辨率设备,剩余像素用黑边补齐

4.4 画面、音频动态同步加速

项目 说明
加速原理 通过时钟倍率让 CPU / PPU / APU 同步加速,而非简单提高帧率
同步策略 以音频时钟为主时钟驱动画面
解决的问题 加速时的画面撕裂与爆音
上限控制 按设备性能自适应,避免低端机强行高倍率

4.5 即时存档与批量导出

  • 游戏内存档:和当年一样,在游戏里找存档点存档(写入卡带 SRAM);
  • 即时存档:把当前整个运行状态冻结保存,每个 ROM 提供多个存档位;
  • 存档管理 :查看时间戳、单独加载或删除,支持批量导出,换设备时直接迁移;
  • 自动落盘:基于核心的"脏标志"机制自动写盘,不需要用户手动保存。

图  存档管理:存档批量导出,快速加载历史存档

4.6 自研测试 ROM《宇宙探索》

模拟器最怕的不是性能,而是"这个游戏能跑、那个游戏花屏"。

只靠现成商业游戏去测,会遇到两个问题:一是覆盖面靠运气,二是出问题时说不清是模拟器的错还是测试用例本身的限制。

所以我写了一个 GBC 测试程序《宇宙探索》------用真实的 GBC 硬件行为去跑,能直接暴露出 PPU 渲染、精灵优先级、滚动、中断时序等环节的问题。有了它,兼容性验证从"碰运气"变成了"有靶子"。

图  自研 GBC 测试 ROM《宇宙探索》

图 9 游戏运行画面

4.7 隐私与无广告

项目 做法
权限 未申请敏感权限,权限列表为空
文件访问 使用 HarmonyOS 安全访问机制:只能访问用户主动选定的文件,不读取存储目录中的其它内容
广告 纯净无广告
隐私政策 查看隐私政策

五、如何上架鸿蒙应用商店

5.1 上架前的三件事

顺序 事项 说明
1 开发者账号与实名认证 个人开发者需完成实名认证,企业需企业认证;未实名无法发布
2 APP 备案(核准) 依据工信部要求,上架前须完成 APP 备案并提供备案号;这是硬门槛
3 软著(建议) 华为官方原文将其列为"非必选资质",但建议申请以保护知识产权

特别提醒:先把备案办了。备案有周期,等开发完才想起来办,会把上架时间整体往后拖。

5.2 签名与打包

HarmonyOS 应用通过数字证书(.cer) 和 Profile 文件(.p7b) 保证完整性,最终打包为 App Pack(.app) 用于上架。

步骤 操作
生成密钥 DevEco Studio → Build → Generate Key and CSR,生成 .p12 密钥库与 CSR 文件
申请证书 在 AppGallery Connect 用 CSR 申请发布证书 .cer
申请 Profile 创建发布 Profile .p7b(需绑定包名与证书)
打包 Build → Build App(s)/Hap(s),产出 .app / .hap
校验 用 hap-sign-tool 或 DevEco 自带校验确认签名有效
bash 复制代码
# 通过命令行构建(CI 场景常用)
hvigorw assembleHap --mode module -p product=default -p buildMode=release

踩坑提示 :调试证书与发布证书不能混用。用调试证书打出来的包上传会被直接拒绝,错误信息往往不直观,我第一次上架就在这卡了一会儿。

5.3 在 AppGallery Connect 提交

#mermaid-svg-1WZxzKMmOv5WqOlw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1WZxzKMmOv5WqOlw .error-icon{fill:#552222;}#mermaid-svg-1WZxzKMmOv5WqOlw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1WZxzKMmOv5WqOlw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1WZxzKMmOv5WqOlw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1WZxzKMmOv5WqOlw .marker.cross{stroke:#333333;}#mermaid-svg-1WZxzKMmOv5WqOlw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1WZxzKMmOv5WqOlw p{margin:0;}#mermaid-svg-1WZxzKMmOv5WqOlw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster-label text{fill:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster-label span{color:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster-label span p{background-color:transparent;}#mermaid-svg-1WZxzKMmOv5WqOlw .label text,#mermaid-svg-1WZxzKMmOv5WqOlw span{fill:#333;color:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw .node rect,#mermaid-svg-1WZxzKMmOv5WqOlw .node circle,#mermaid-svg-1WZxzKMmOv5WqOlw .node ellipse,#mermaid-svg-1WZxzKMmOv5WqOlw .node polygon,#mermaid-svg-1WZxzKMmOv5WqOlw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1WZxzKMmOv5WqOlw .rough-node .label text,#mermaid-svg-1WZxzKMmOv5WqOlw .node .label text,#mermaid-svg-1WZxzKMmOv5WqOlw .image-shape .label,#mermaid-svg-1WZxzKMmOv5WqOlw .icon-shape .label{text-anchor:middle;}#mermaid-svg-1WZxzKMmOv5WqOlw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1WZxzKMmOv5WqOlw .rough-node .label,#mermaid-svg-1WZxzKMmOv5WqOlw .node .label,#mermaid-svg-1WZxzKMmOv5WqOlw .image-shape .label,#mermaid-svg-1WZxzKMmOv5WqOlw .icon-shape .label{text-align:center;}#mermaid-svg-1WZxzKMmOv5WqOlw .node.clickable{cursor:pointer;}#mermaid-svg-1WZxzKMmOv5WqOlw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1WZxzKMmOv5WqOlw .arrowheadPath{fill:#333333;}#mermaid-svg-1WZxzKMmOv5WqOlw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1WZxzKMmOv5WqOlw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1WZxzKMmOv5WqOlw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1WZxzKMmOv5WqOlw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1WZxzKMmOv5WqOlw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1WZxzKMmOv5WqOlw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster text{fill:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw .cluster span{color:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1WZxzKMmOv5WqOlw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1WZxzKMmOv5WqOlw rect.text{fill:none;stroke-width:0;}#mermaid-svg-1WZxzKMmOv5WqOlw .icon-shape,#mermaid-svg-1WZxzKMmOv5WqOlw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1WZxzKMmOv5WqOlw .icon-shape p,#mermaid-svg-1WZxzKMmOv5WqOlw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1WZxzKMmOv5WqOlw .icon-shape .label rect,#mermaid-svg-1WZxzKMmOv5WqOlw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1WZxzKMmOv5WqOlw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1WZxzKMmOv5WqOlw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1WZxzKMmOv5WqOlw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
登录 AppGallery Connect
APP 与元服务 创建应用
填写应用名称、包名、分类
上传软件包

应用上架 > 软件包管理
填写版本信息与更新说明
配置隐私政策链接、权限说明
上传素材:图标 + 截图 + 介绍
提交审核
审核是否通过?
按审核意见修改后重新提交
分阶段发布 / 全网发布
上架完成,可在应用市场搜索到

关键填写项:

项目 注意点
应用分类 本应用选择「工具」;分类会影响所需资质(游戏类需版号)
隐私政策 必须是可公开访问的链接,且应用内可查
权限说明 每个申请的权限都要写明使用场景;本应用未申请敏感权限,这一项反而简单
素材 图标、截图有尺寸与数量要求,建议按平台提供的两套尺寸(手机 / 平板)分别上传
更新说明 每版都要写清楚改了什么,审核会看

5.4 审核容易被驳回的几类问题

驳回类型 常见原因 规避方法
资质缺失 未提供 APP 备案号 上架前完成备案
隐私政策问题 链接打不开、内容空泛、未列出权限用途 逐条写明收集的信息类型与用途
权限越界 申请了与功能无关的权限 权限最小化,用系统安全访问机制替代
使用调试证书 用调试证书打了发布包 严格区分调试/发布证书
内容违规 含侵权内容、引导下载盗版 不内置侵权资源,不提供下载入口
名称/包名不一致 应用名称与资质文件不符 保持各处名称一致

对模拟器类应用特别提醒 :审核会重点关注是否内置游戏资源、是否提供下载入口。这两条是明确的红线,从产品设计阶段就要避开------这也是我把"正版 ROM 校验"做进产品的原因之一。

5.5 我的上架结果

项目 结果
应用名称 GBC模拟器
分类 工具
上架状态 已上架,可正常下载
当前版本 1.1.3(versionCode 1000013)
安装量 < 1 万次安装
用户评分 4.5 分(19 人评分,其中 5 星 15 人)

对一个个人开发者的工具类应用来说,这个开局我认为是达标的------真正有价值的反馈往往来自低分评价,它比好评更能指出兼容性问题。

5.6 上架检查清单

  • 开发者账号已完成实名认证
  • APP 备案已通过并拿到备案号
  • 软著 / 电子版权证书已申请(建议项)
  • 隐私政策已上线且链接可公开访问
  • 应用内可查看隐私政策与第三方开源组件声明
  • 用发布证书打包,已校验签名有效
  • 应用分类选择正确(工具 / 游戏,影响资质要求)
  • 权限清单与功能一一对应,无多余权限
  • 图标与截图素材齐备,覆盖手机 / 平板两套尺寸
  • 版本更新说明已填写
  • 未内置任何游戏资源、未提供 ROM 下载入口
  • 已在真机完成核心功能回归测试

下载与参考资料

下载

开发参考

相关阅读


相关推荐
m0_738185821 小时前
Flutter 鸿蒙化实战:umeng_analytics_plugin 适配 OpenHarmony,友盟统计
flutter·华为·harmonyos·鸿蒙
todoitbo2 小时前
把 XAMPP 搬到鸿蒙 PC:在鸿蒙 PC 上跑 Apache httpd 2.4.69实战项目
华为·apache·harmonyos·鸿蒙·开源鸿蒙·鸿蒙pc
JoyCong19982 小时前
即插即用,组合键可用:ToDesk 4820版本强化鸿蒙远控键鼠能力
华为·智能手机·harmonyos·远程工作·ipad
程序猿追2 小时前
漏字、换词序、空查询:用 FuzzyKot 做一次鸿蒙搜索交互排查
华为·交互·harmonyos
懿路向前2 小时前
HarmonyOS 粒子特效实战:Particle 挂载 ≠ 显示:emitRate/count 真实语义与同帧黑缝(HarmonyOS ArkUI)
华为·harmonyos
贾伟康3 小时前
【化学实验室|02】HarmonyOS ArkTS 实验结果载荷实战:用强类型模型隔离页面参数与结果展示
harmonyos·arkts·canvas·arkui·化学实验
万物智能信息科技3 小时前
气体传感器MQ-2芯片驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·嵌入式硬件·华为·开源·harmonyos
懿路向前3 小时前
HarmonyOS 粒子特效实战:ArkTS V2 状态管理三坑:@Local 判据 / 引用替换 / 双通道更新(HarmonyOS)
华为·harmonyos
蒿豪郝灬号4 小时前
UniApp接入鸿蒙版阿里百川
华为·uni-app·harmonyos