Appearance
资源转换管线 — 代码走读
脚本概览
本项目的资源转换由两个 Python 脚本完成:
generate_wasm_assets.py
整体结构
主函数
python
def main():
"""主入口:扫描源码并生成 WASM 资源文件"""
# 1. 确定 C 源文件搜索路径
source_dirs = ['src/', 'data/', 'graphics/']
# 2. 收集所有资源引用
resources = []
for dir_path in source_dirs:
for c_file in find_c_files(dir_path):
resources.extend(scan_for_incgfx(c_file))
resources.extend(scan_for_incbin(c_file))
# 3. 去重并排序
resources = deduplicate(resources)
# 4. 处理每个资源
for res in resources:
if res.type == 'gfx':
convert_gfx_resource(res)
elif res.type == 'bin':
convert_bin_resource(res)
# 5. 生成输出文件
generate_output_header(resources, 'generated/wasm_assets.h')
generate_output_data(resources, 'generated/wasm_assets.c')INCGFX 扫描
python
import re
# 匹配 INCGFX 宏调用的正则表达式
INCGFX_RE = re.compile(
r'INCGFX\s*\(\s*"([^"]+)"\s*\)'
)
def scan_for_incgfx(c_file_path):
"""扫描 C 文件中的所有 INCGFX 宏调用"""
results = []
with open(c_file_path, 'r') as f:
content = f.read()
for match in INCGFX_RE.finditer(content):
resource_path = match.group(1)
results.append({
'type': 'gfx',
'path': resource_path,
'source_file': c_file_path,
'match_text': match.group(0),
})
return results正则表达式解析:
INCGFX— 匹配宏名称\s*\(\s*— 匹配左括号及周围空白"([^"]+)"— 匹配并捕获双引号内的文件路径\s*\)— 匹配右括号及前面空白
INCBIN 扫描
python
INCBIN_RE = re.compile(
r'INCBIN\s*\(\s*"([^"]+)"\s*\)'
)
def scan_for_incbin(c_file_path):
"""扫描 C 文件中的所有 INCBIN 宏调用"""
results = []
with open(c_file_path, 'r') as f:
content = f.read()
for match in INCBIN_RE.finditer(content):
resource_path = match.group(1)
# 根据后缀判断是否需要特殊处理
suffix = resource_path.rsplit('.', 1)[-1] if '.' in resource_path else ''
results.append({
'type': 'bin',
'path': resource_path,
'source_file': c_file_path,
'format': suffix, # 'bin', 'lz77', 'rl', 等
})
return results图形资源转换
python
def convert_gfx_resource(resource):
"""将 GBA 图形资源转换为 C 数组"""
path = resource['path']
# 根据文件扩展名确定格式
if path.endswith('.4bpp'):
# 4bpp: 每像素 4 位, 16 色
data = read_binary_file(path)
resource['data'] = data
resource['element_type'] = 'u8'
resource['format_desc'] = '4bpp (16 colors)'
elif path.endswith('.8bpp'):
# 8bpp: 每像素 8 位, 256 色
data = read_binary_file(path)
resource['data'] = data
resource['element_type'] = 'u8'
resource['format_desc'] = '8bpp (256 colors)'
elif path.endswith('.pal'):
# 调色板文件: 16bit BGR555 颜色数组
data = read_binary_file(path)
resource['data'] = data
resource['element_type'] = 'u16'
resource['format_desc'] = 'palette (BGR555)'
def read_binary_file(path):
"""读取二进制文件,返回字节数组"""
with open(path, 'rb') as f:
return f.read()头文件生成
python
def generate_output_header(resources, output_path):
"""生成包含 extern 声明的头文件"""
lines = [
"// 自动生成 - 请勿手动修改",
"// 由 generate_wasm_assets.py 生成",
"#ifndef WASM_ASSETS_H",
"#define WASM_ASSETS_H",
"",
'#include "global.h"',
"",
]
for res in resources:
# 生成合法的 C 变量名
var_name = sanitize_path_to_identifier(res['path'])
elem_type = res.get('element_type', 'u8')
data = res.get('data', b'')
lines.append(f"// {res['path']} ({res.get('format_desc', 'binary')})")
lines.append(f"extern const {elem_type} {var_name}[{len(data)}];")
lines.append("")
lines.append("#endif // WASM_ASSETS_H")
with open(output_path, 'w') as f:
f.write('\n'.join(lines))wasm_asm_data.py
整体结构
汇编文件解析
python
class AsmDataParser:
"""GBA 汇编数据文件解析器"""
def __init__(self):
self.data_buffer = bytearray()
self.symbols = {} # 符号名 → 偏移量
self.current_offset = 0
self.output_sections = [] # 每个全局符号对应一个数据段
def parse_file(self, filepath):
"""解析单个汇编文件"""
with open(filepath, 'r') as f:
lines = f.readlines()
self._parse_lines(lines)
def _parse_lines(self, lines):
"""逐行解析汇编指令"""
for line in lines:
# 去除注释和空白
line = line.split('//')[0].split('@')[0].strip()
if not line:
continue
# 标签
if line.endswith(':') and not line.startswith('.'):
label = line[:-1].strip()
self.symbols[label] = self.current_offset
continue
# 数据指令
if line.startswith('.byte'):
self._parse_byte_directive(line)
elif line.startswith('.halfword') or line.startswith('.hword'):
self._parse_halfword_directive(line)
elif line.startswith('.word'):
self._parse_word_directive(line)
elif line.startswith('.align'):
self._parse_align_directive(line)
elif line.startswith('.include'):
self._parse_include_directive(line)
elif line.startswith('.space'):
self._parse_space_directive(line)
elif line.startswith('.ascii') or line.startswith('.string'):
self._parse_string_directive(line)数据指令解析
python
def _parse_byte_directive(self, line):
"""解析 .byte value1, value2, ..."""
values_str = line[5:].strip() # 去除 ".byte"
values = self._parse_value_list(values_str)
for val in values:
self.data_buffer.append(val & 0xFF)
self.current_offset += 1
def _parse_halfword_directive(self, line):
"""解析 .halfword value1, value2, ..."""
# 确保对齐到 2 字节
self._align_to(2)
values_str = line.split(None, 1)[1].strip()
values = self._parse_value_list(values_str)
for val in values:
# GBA 为小端序
self.data_buffer.append(val & 0xFF)
self.data_buffer.append((val >> 8) & 0xFF)
self.current_offset += 2
def _parse_word_directive(self, line):
"""解析 .word value1, value2, ..."""
# 确保对齐到 4 字节
self._align_to(4)
values_str = line.split(None, 1)[1].strip()
values = self._parse_value_list(values_str)
for val in values:
# 32 位小端序
self.data_buffer.append(val & 0xFF)
self.data_buffer.append((val >> 8) & 0xFF)
self.data_buffer.append((val >> 16) & 0xFF)
self.data_buffer.append((val >> 24) & 0xFF)
self.current_offset += 4
def _parse_align_directive(self, line):
"""解析 .align n — 对齐到 2^n 字节边界"""
n = int(line.split()[1])
self._align_to(1 << n)
def _align_to(self, boundary):
"""填充零字节直到对齐到指定边界"""
padding = (boundary - (self.current_offset % boundary)) % boundary
self.data_buffer.extend(b'\x00' * padding)
self.current_offset += paddingC 代码输出
python
def generate_c_output(self, output_path):
"""将解析的数据生成 C 源文件"""
lines = [
"// 自动生成 - 请勿手动修改",
"// 由 wasm_asm_data.py 生成",
'#include "global.h"',
"",
]
for symbol_name, offset in self.symbols.items():
# 计算此符号的数据段大小
# (到下一个符号或缓冲区末尾)
next_offsets = [o for o in self.symbols.values() if o > offset]
end = min(next_offsets) if next_offsets else len(self.data_buffer)
data = self.data_buffer[offset:end]
if not data:
continue
# 生成 C 数组
lines.append(f"const u8 {symbol_name}[] = {{")
# 每行 16 字节
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_values = ', '.join(f'0x{b:02X}' for b in chunk)
lines.append(f" {hex_values},")
lines.append("};")
lines.append("")
with open(output_path, 'w') as f:
f.write('\n'.join(lines))值解析工具
python
def _parse_value_list(self, values_str):
"""解析逗号分隔的值列表,支持十进制、十六进制和符号"""
values = []
for item in values_str.split(','):
item = item.strip()
if not item:
continue
# 十六进制: 0x...
if item.startswith('0x') or item.startswith('0X'):
values.append(int(item, 16))
# 十进制数字
elif item.isdigit() or (item.startswith('-') and item[1:].isdigit()):
values.append(int(item))
# 已知常量 (从常量头文件解析)
elif item in self.known_constants:
values.append(self.known_constants[item])
# 未知符号 — 留空待后续解析
else:
values.append(0) # 占位
print(f"Warning: 未解析的符号: {item}")
return values关键函数索引
| 函数名 | 所属脚本 | 说明 |
|---|---|---|
main() | generate_wasm_assets.py | 主入口:扫描源码并生成资源文件 |
scan_for_incgfx(c_file) | generate_wasm_assets.py | 扫描单个 C 文件中的 INCGFX 宏 |
scan_for_incbin(c_file) | generate_wasm_assets.py | 扫描单个 C 文件中的 INCBIN 宏 |
convert_gfx_resource(res) | generate_wasm_assets.py | 转换图形资源为 C 数组 |
convert_bin_resource(res) | generate_wasm_assets.py | 转换二进制资源 |
generate_output_header(res, path) | generate_wasm_assets.py | 生成 extern 声明头文件 |
generate_output_data(res, path) | generate_wasm_assets.py | 生成数据定义 C 文件 |
deduplicate(resources) | generate_wasm_assets.py | 去除重复的资源引用 |
parse_file(filepath) | wasm_asm_data.py (AsmDataParser) | 解析汇编文件入口 |
_parse_byte_directive(line) | wasm_asm_data.py (AsmDataParser) | 解析 .byte 指令 |
_parse_halfword_directive(line) | wasm_asm_data.py (AsmDataParser) | 解析 .halfword 指令 |
_parse_word_directive(line) | wasm_asm_data.py (AsmDataParser) | 解析 .word 指令 |
_parse_align_directive(line) | wasm_asm_data.py (AsmDataParser) | 解析 .align 对齐指令 |
_align_to(boundary) | wasm_asm_data.py (AsmDataParser) | 计算并插入对齐填充 |
generate_c_output(path) | wasm_asm_data.py (AsmDataParser) | 生成 C 源文件输出 |
数据流总览
相关阅读:
- 资源转换管线概念 — 理解 INCGFX/INCBIN 宏和转换原理
- WASM 编译管线代码走读 — 了解生成的 C 文件如何编译到 WASM
- GBA 硬件模拟代码走读 — 了解资源数据在运行时的加载和渲染