Temple OS源码研究
博客:源码解读
TOS源码: https://github.com/cia-foundation/TempleOS
启动过程: https://minexew.github.io/2020/02/27/templeos-loader-part1.html
建设性看法:http://www.codersnotes.com/notes/a-constructive-look-at-templeos/
移植文字游戏:https://www.jwhitham.org/2015/07/porting-third-party-programs-to-templeos.html
常用命令
TaskRep; //任务管理器
Kill(0x00100300); //杀死进程
Unzip("test.HC.Z");
Type("StadiumBG.GR.Z"); //打印DolDoc(文档 或 精灵Spirit)
Reboot;
#include "test.HC" //立即执行
Ctrl+F1 快速输入/召唤自动补全
Ctrl+Alt+X 类似linux下的Ctrl+C,中断当前任务
Ctrl+R 创建/编辑spirit
Ctrl+T 显示doldoc为raw text
图片嵌入
详见~/Demo/Games/Stadium/
: https://github.com/cia-foundation/TempleOS/tree/archive/Demo/Games/Stadium
找demo
看注释,说是.DD
生成了.GR
文件。我们可以写一个python,来将任意图片,转换成.DD
文件。
DolDoc
打开StadiumBG.DD
,文件头部有:
$SP,"<01>",BI=1$
$
:类似Minecraft的§
,表示非普通文本,不过要头尾都有。
SP
: Spirit精灵
"<01>"
: 占位符,留空则不显示。类似markdown的![占位符文字](图片URL)
BI
: 类似唯一id。每创建一个新spirit时+1。
.GR
后面跟着二进制数据,结合GR文件结构: 一个字节,但最多16种颜色。0x00~0x0F,透明为0xFF
首先Ctrl+R,得到这是一个BitMap,长640,高472
(472)₁₀=(1D8)₁₆,发现是little endian方式存储(x86)。有点反人类,但对机器友好。
得到结构:
"$SP,"",BI=1$"; //DD文件,前面全文字,后面全bin
//BI头:
0x00;
0x0100 0000 0000 0000; //BI值:0x0200 0000 0000 0000
0x129c 0400; //BI内所有元素的字节长(方便访问下一个BI)
0x0100 0000; //固定,但可能是某个会递增的id?
//同一BI内,元素头:
0x17; //固定,代表该元素为bitmap
I32 offset_width;
I32 offset_height;
U32 width; //建议为8的倍数
U32 height;
U8 body[640*472]={
0x0F,0x0F,...,0x0F, //每行640个0x0F,默认为白色0x0F
// 0xFF... //每行尾随补充一定数量的0xFF,使得一行字节数能**凑齐** 8的倍数。如一行640,则无须补充0xFF;若一行14,则还需补充2个0xFF,凑到16个字节每行
}[472]; //共有472行
0x00; //每个**元素**的EOF,body最后一行最后一字节置为0x00,后延续一次每行的0xFF数量。
0x00; //文件末尾的EOF。若为最后一个body,则body的最后一行的最后一字节不必为0xFF。如0x0f 0x0f (0xFF...) 0x00 EOF
py
#!/bin/env python
import struct
import cv2
import numpy as np
import taichi as ti
from typing import Literal
from PIL import Image
import os
import platform
OS = platform.system()
DEBUG=True
WIDTH=640
HEIGHT=480
MAX=255
BAYER_M=(4,1) # 4x4 bayer matrix, scale by 1
DD_SAVE='/home/n/document/code/templeos-plus/o.DD'
TMP_PNG='tmp_dither_{}.PNG'
TMP_DIR={
'Linux':'/tmp/',
'Windows':'C:/Users/lenovo/AppData/Local/Temp/',
}
dither_algorithm:Literal['bayer','floyd']='floyd'
interpolation=cv2.INTER_LANCZOS4
"""
cv2.INTER_NEAREST :最近邻插值。最快,但通常效果最差
cv2.INTER_LINEAR :双线性插值。默认,适用于大多数情况,平衡了速度和图像质量。
cv2.INTER_CUBIC :三次样条插值。计算复杂度较高,但图像质量较好,适用于对图像质量要求较高的场景。
cv2.INTER_AREA :区域插值。适用于缩小图像,可以减少锯齿效应。
cv2.INTER_LANCZOS4 :Lanczos 插值。使用 8x8 邻域进行插值,计算复杂度最高,但图像质量最好,适用于对图像质量要求极高的场景。
"""
BIS = [
{
'text': '<1>',
'elem': [
{
'img': './input.jpg',
'w': 640,
},
{
'img': './transparent.png',
'h': 640,
'offset_w': -320,
},
]
},
{
'text': '<2>',
'elem': [
{
'img': './input.jpg',
'w': 640, # 同时指定h,w时,强制拉伸
'h': 640,
},
{
'img': './transparent.png',
'offset_h': -320,
},
]
},
]
cmd = f'\\rm {TMP_DIR[OS]}{TMP_PNG.format('*')}'
# cmd = f'\\rm {TMP_DIR[OS]}tmp*.PNG'
print('cmd',cmd)
os.system(cmd) if OS == 'Linux' else None
# Dont't modify below 不要修改以下内容
TEXT_SP='$SP,"{}",BI={}$'
HEAD_BI='''
00
{} 0000 0000
{}
0100 0000
'''
TRANSPARENT = 0xFF
PALETTE = [
0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
]
PALETTE = [(c & 0xFF, c >> 8 & 0xFF, c>>16 & 0xFF) for c in PALETTE] #BGR for opencv default color space
# print('🎨 palette',palette) if DEBUG else None
CHANNEL = len(PALETTE[0])
PALETTE_SET = [ sorted(list(set(c[ch] for c in PALETTE))) for ch in range(CHANNEL)]
PALETTE_SET = [ [0] if ch[0]!=0 else [] + ch + [0xFFFFFF] for ch in PALETTE_SET]
print('PALETTE_SET',PALETTE_SET) if DEBUG else None
def show_image(img,lib:Literal['cv2','pil','sys']='pil'):
"""
- cv2: 无法显示alpha透明通道,但不会生成临时文件
- pil: 可以显示alpha通道。在Linux下由于调用xdg-open,会生成临时文件
"""
if lib == 'sys':
if OS == 'Linux':
os.environ["QT_QPA_PLATFORM"] = "dxcb"
os.system(f'xdg-open "{img}"')
elif OS == 'Windows':
os.system(f'start "{img}"')
return
if isinstance(img, str):
img = cv2.imread(img)
elif img.dtype != 'uint8':
img = np.clip(img, 0, MAX) # 将图片像素值限制在 0~255 之间
img = img.astype(np.uint8)
if lib == 'cv2':
os.environ.pop("QT_QPA_PLATFORM", None) if OS == 'Linux' else None
cv2.imshow('Image', img)
while True:
key = cv2.waitKey(1) & 0xFF
if key == 27 or key == 32: # ESC或空格
break
cv2.destroyAllWindows()
elif lib == 'pil':
os.environ["QT_QPA_PLATFORM"] = "dxcb" if OS == 'Linux' else None # 根据不同操作系统,灵活调整
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
pil_img = Image.fromarray(img)
pil_img.show()
def get_fit_hw(img,hw) -> tuple[int,int]:
h,w = hw
old_h, old_w = img.shape[:2]
if h and w: # 强制拉伸,局部dic['h'] > 全局HEIGHT
return h, w
elif w:
new_w = w
new_h = int(new_w * old_h / old_w)
elif h:
new_h = h
new_w = int(new_h * old_w / old_h)
elif WIDTH>0 and HEIGHT>0:
return WIDTH,HEIGHT
elif WIDTH>0:
new_w = WIDTH
new_h = int(new_w * old_h / old_w)
elif HEIGHT>0:
new_h = HEIGHT
new_w = int(new_h * old_w / old_h)
return new_h, new_w
def hsv(img, h=0, s=1, v=1):
"""调整图像的色调、饱和度、亮度"""
# channel = img.shape[2]
# if channel == 4:
# alpha = img[:, :, 3]
# img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img[:, :, 0] = np.clip((img[:, :, 0] + h)%MAX, 0, 255)
img[:, :, 1] = np.clip(img[:, :, 1] * s, 0, 255)
img[:, :, 2] = np.clip(img[:, :, 2] * v, 0, 255)
img = cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
# if channel == 4:
# img = np.dstack((img, alpha))
return img
def bayer_matrix(n: int = 4):
"""
生成任意大小的bayer矩阵
但其实,n=2,3,4,8
为什么8是最大值?因为256=2^8,所以8x8的bayer矩阵可以覆盖256色
"""
M2 = np.array([[0, 2],
[3, 1]], dtype=np.uint8)
if n == 3:
return [[0, 7, 3],
[6, 5, 2],
[4, 1, 8]]
elif np.log2(n) % 1 != 0: # n不是2的幂次方,%1取小数部分
raise ValueError('n must be a power of 2')
elif n == 2:
return M2
else:
half = n // 2
M = np.zeros((n, n), dtype=np.uint8)
M[:half, :half] = bayer_matrix(half) * 4
M[:half, half:] = bayer_matrix(half) * 4 + 2 * np.eye(half)
M[half:, :half] = bayer_matrix(half) * 4 + 3 * np.eye(half)
M[half:, half:] = bayer_matrix(half) * 4 + np.eye(half)
return M
def gen_bayerM(n=4,scale=1):
bayerM = ti.ndarray(shape=(n*scale, n*scale), dtype=ti.u8)
bayerRaw = np.matrix(bayer_matrix(n), dtype=np.uint8)
bayerRaw = np.kron(bayerRaw, np.ones((scale,scale), dtype=np.uint8))
print(bayerRaw,'← bayerM') if DEBUG else None
bayerM.from_numpy(bayerRaw)
return bayerM
type_2d_vec = ti.types.ndarray(element_dim=1,ndim=2)
type_2d = ti.types.ndarray(element_dim=0,ndim=2)
# type_2d_vec = object
# type_2d = object
@ti.func
def get_closest_bgr(old_pixel):
min_distance = 0xFFFFFFF
new_pixel = ti.Vector([0,0,0],dt=ti.i32)
index = 0
i = 0
r, g, b = old_pixel
for c in ti.static(PALETTE):
cr, cg, cb = c
distance = ti.int32((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
if distance < min_distance:
min_distance = distance
new_pixel = c
index = i
i+=1
return [new_pixel, index]
@ti.kernel
def dither_bayer(img:type_2d_vec, bayerM:type_2d,hex:ti.template(),alpha:type_2d):
h = img.shape[0]
w = img.shape[1]
n = bayerM.shape[0]
has_alpha = alpha.shape[0] > 1
# print('has_alpha',has_alpha) if DEBUG else None
for i in range(h):
for j in range(w):
th = bayerM[i % n, j % n] / (n**2)
for ch in ti.static(range(CHANNEL)):
for b in ti.static(range(len(PALETTE_SET[ch])-1)):
if PALETTE_SET[ch][b] < img[i, j][ch]/th < PALETTE_SET[ch][b+1]:
img[i, j][ch] = PALETTE_SET[ch][b]
img[i, j],index = get_closest_bgr(img[i,j])
if has_alpha:
if alpha[i, j] > th * MAX:
alpha[i, j] = MAX # 透明度大于阈值,设为不透明
hex[i, j] = index
else:
alpha[i, j] = 0
hex[i, j] = TRANSPARENT
else:
hex[i, j] = index
# fix taichi
h = img.shape[0]
w = img.shape[1]
for i in range(h):
if i%512==0: # taichi cpu fix
for j in range(w):
img[i, j],index = get_closest_bgr(img[i, j])
@ti.kernel
def dither_floyd(img:type_2d_vec,hex:ti.template(),alpha:type_2d):
h = img.shape[0]
w = img.shape[1]
has_alpha = alpha.shape[0] > 1
# print('has_alpha',has_alpha) if DEBUG else None
for i in range(h):
for j in range(w):
oldpixel = img[i, j]
oldpixel = ti.math.clamp(img[i, j],0,MAX)
newpixel,index = get_closest_bgr(oldpixel)
quant_error = oldpixel - newpixel
img[i, j] = newpixel
if j+1<w:
img[i, j + 1] += quant_error * 7 // 16
if i+1<h:
if j-1>=0:
img[i + 1, j - 1] += quant_error * 3 // 16
img[i + 1, j] += quant_error * 5 // 16
if j+1<w:
img[i + 1, j + 1] += quant_error // 16
if has_alpha:
old_a = alpha[i, j]
if alpha[i, j] * 2 > MAX:
alpha[i, j] = MAX
hex[i, j] = index
else:
alpha[i, j] = 0
hex[i, j] = TRANSPARENT
alpha_error = old_a - alpha[i, j]
if j+1<w:
alpha[i, j + 1] += alpha_error * 7 // 16
if i+1<h:
if j-1>=0:
alpha[i + 1, j - 1] += alpha_error * 3 // 16
alpha[i + 1, j] += alpha_error * 5 // 16
if j+1<w:
alpha[i + 1, j + 1] += alpha_error // 16
else:
hex[i, j] = index
# fix taichi
h = img.shape[0]
w = img.shape[1]
for i in range(h):
if i%512==0: # taichi cpu fix
for j in range(w):
img[i, j],index = get_closest_bgr(img[i, j])
hex[i, j] = index
def dither_main(img,is_last_one:False):
h,w = img.shape[:2]
hex = bytearray()
FF_count = 8 - w%8 if w%8 else 0
FF = b'\xFF' * FF_count
hex2d = ti.field(dtype=ti.u8,shape=(h,w))
alpha=np.zeros((1,1),dtype=np.int32)
if img.shape[2] == 4: # 有alpha通道
alpha = img[:,:,3]
alpha = alpha.astype(np.int32)
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
if dither_algorithm == 'bayer':
img = hsv(img, s=1, v=.3) # 💡 这里默认调低了亮度,可以根据实际情况调整
img = img.astype(np.int32)
alpha = np.ascontiguousarray(alpha,dtype=np.int32)
if dither_algorithm == 'floyd':
dither_floyd(img,hex2d,alpha)
elif dither_algorithm == 'bayer':
dither_bayer(img, gen_bayerM(*BAYER_M), hex2d, alpha)
hex2d = hex2d.to_numpy()
# 将img与alpha合并
if alpha.any():
img = np.dstack((img, alpha))
img = img.astype(np.uint8)
for y in range(h):
for x in range(w):
hex.extend(struct.pack('<B', hex2d[y, x]))
hex.extend(FF)
if not is_last_one:
hex[-FF_count-1] = 0x00
return img,bytes(hex)
def get_bitmap(elem,is_last_one=False) -> bytes:
BITMAP = struct.pack('<B',0x17)
bi = b''
for dic in elem:
img_path = dic['img']
old_hw = [dic.get('h', None),dic.get('w', None)]
offset_wh = (dic.get('offset_w', 0),dic.get('offset_h', 0))
wh = struct.pack('<i',offset_wh[0])
wh += struct.pack('<i',offset_wh[1])
img = cv2.imread(img_path,cv2.IMREAD_UNCHANGED)
if img is None:
raise Exception('Image not found: ' + img_path)
print('raw_size',img.shape) if DEBUG else None
new_h,new_w = get_fit_hw(img,old_hw)
img = cv2.resize(img, (new_w,new_h), interpolation=interpolation)
wh += struct.pack('<I',new_w)
wh += struct.pack('<I',new_h)
print('fitted_size',img.shape) if DEBUG else None
# print('hex_wh',struct.pack('<H',new_w),struct.pack('<H',new_h))
img,body = dither_main(img,is_last_one)
if DEBUG:
if OS == 'Windows':
show_image(img,'cv2')
else:
save_path = TMP_DIR[OS] + TMP_PNG.format(os.path.basename(img_path))
print('save_path:',save_path)
cv2.imwrite(save_path,img)
print('bitmap head:',(BITMAP + wh).hex(' ',2)) if DEBUG else None
bi += BITMAP + wh + body
return bi
def get_BIs(bis) -> bytes:
hex = b''
for i in range(len(bis)):
hex+=TEXT_SP.format(bis[i]['text'],i+1).encode()
for i in range(len(bis)):
id = i+1
bmap=get_bitmap(bis[i]['elem'], id == len(bis))
hex_len=struct.pack('<I',len(bmap)+1).hex(' ',2) # +1
id_bi = struct.pack('<I',id).hex(' ',2)
head_bi=bytes.fromhex(HEAD_BI.format(id_bi,hex_len).strip()) # BI_ID's max is unsigned short = 2^16
print('BI head:',HEAD_BI.format(id_bi,hex_len)) if DEBUG else None
hex+=head_bi
hex+=bmap
return hex + b'\x00'
ti.init(arch=ti.cpu,debug=False)
hex=get_BIs(BIS)
with open(DD_SAVE, 'wb') as f:
f.write(hex)
if OS != 'Windows':
pngs = os.popen(f'\\ls {TMP_DIR[OS]}{TMP_PNG.format('*')}').read().split('\n')
show_image(pngs[0],'sys')