diff --git a/tools/boot_dvd.asm b/tools/boot_dvd.asm new file mode 100644 index 00000000..1a92fffe --- /dev/null +++ b/tools/boot_dvd.asm @@ -0,0 +1,227 @@ +; boot_dvd.asm - NASM translation of TempleOS BootDVD.HC +; Boot loader for CD/DVD-ROM booting via El Torito +; Assembled with: nasm -f bin -o boot_dvd.bin boot_dvd.asm + +BITS 16 + +; Constants from KernelA.HH +%define BLK_SIZE_BITS 9 +%define BLK_SIZE (1 << BLK_SIZE_BITS) ; 512 +%define DVD_BLK_SIZE (4 * BLK_SIZE) ; 2048 +%define DVD_BOOT_LOADER_SIZE DVD_BLK_SIZE ; 2048 +%define BOOT_RAM_BASE 0x07C00 +%define BOOT_RAM_LIMIT 0x97000 +%define BOOT_STK_SIZE BLK_SIZE ; 512 +%define BOOT_SRC_DVD 4 + +; BOOT_HIGH_LOC = (BOOT_RAM_LIMIT - (BOOT_STK_SIZE + DVD_BOOT_LOADER_SIZE)) >> 4 +; = (0x97000 - (512 + 2048)) >> 4 = (0x97000 - 0xA00) >> 4 = 0x96600 >> 4 = 0x9660 +%define BOOT_HIGH_LOC 0x9660 + +BDVD_START: + cld + mov ax, BOOT_HIGH_LOC + mov es, ax + + cli + mov ss, ax + mov sp, BOOT_STK_SIZE + DVD_BOOT_LOADER_SIZE + sti + + ; Determine our load address and copy ourselves to BOOT_HIGH_LOC + call BDVD_GET_RIP +BDVD_GET_RIP: + pop bx + sub bx, BDVD_GET_RIP - BDVD_START + shr bx, 4 + mov ax, cs + add ax, bx + mov ds, ax + mov cx, DVD_BOOT_LOADER_SIZE + xor si, si + xor di, di + rep movsb + + mov ax, BOOT_HIGH_LOC + mov ds, ax + + ; Far jump to relocated code at BOOT_HIGH_LOC:BDVD_MAIN + db 0xEA + dw BDVD_MAIN - BDVD_START, BOOT_HIGH_LOC + +; Data area +BDVD_BIOS_DRV_NUM: db 0 +BDVD_PAGE: db 0 + +; BIOS Disk Address Packet for INT 13h/AH=42h +BDVD_DAP: db 16, 0 ; size=16, reserved=0 + db 1, 0 ; count=1, reserved=0 +BDVD_DAP_BUF: dw 0, 0 ; offset, segment +BDVD_DAP_BLK: dq 0 ; LBA + +BDVD_TEMPLEOS_MSG: db "Loading TempleOS", 0 +BDVD_NOT64_MSG: db "TempleOS requires a 64-bit capable processor.", 13, 10, 0 + +; These fields get patched by build_iso.py with the kernel location +; Offsets from BDVD_START are used for patching +BDVD_BLK_LO: dw 0 ; DVD LBA low word +BDVD_BLK_HI: dw 0 ; DVD LBA high word +BDVD_BLK_CNT: dw 0 ; Number of DVD blocks to read +BDVD_SHIFT_BLKS: dw 0 ; Sub-block alignment offset +BDVD_PROGRESS_STEP: dd 0 +BDVD_PROGRESS_VAL: dd 0 + +; Put a character to screen via BIOS +BDVD_PUT_CHAR: + mov ah, 0x0E + mov bl, 7 + mov bh, [BDVD_PAGE - BDVD_START] + int 0x10 +BDVD_RET: + ret + +; Print null-terminated string at DS:SI +BDVD_PUTS: +.loop: + lodsb + test al, al + jz BDVD_RET + call BDVD_PUT_CHAR + jmp .loop + +; Main entry point (runs after relocation) +BDVD_MAIN: + mov [BDVD_BIOS_DRV_NUM - BDVD_START], dl + + ; Get current video page + mov ah, 0x0F + int 0x10 + mov [BDVD_PAGE - BDVD_START], bh + + ; Check for 64-bit CPU support + mov eax, 0x80000000 + cpuid + cmp eax, 0x80000001 + jb .not64 + + mov eax, 0x80000001 + cpuid + bt edx, 29 + jc .is64 + +.not64: + mov si, BDVD_NOT64_MSG - BDVD_START + call BDVD_PUTS +.hang: + jmp .hang + +.is64: + mov si, BDVD_TEMPLEOS_MSG - BDVD_START + call BDVD_PUTS + + ; Set up ES to point to kernel load area + mov ax, BOOT_RAM_BASE / 16 + mov es, ax + xor ecx, ecx + mov cx, [BDVD_BLK_CNT - BDVD_START] + + ; Calculate progress bar step + mov eax, (80 - 7 - 9) * 65536 ; 64 columns * 65536 + xor edx, edx + div ecx + mov [BDVD_PROGRESS_STEP - BDVD_START], eax + mov dword [BDVD_PROGRESS_VAL - BDVD_START], 0 + + ; Load starting LBA + mov ax, [BDVD_BLK_LO - BDVD_START] + mov dx, [BDVD_BLK_HI - BDVD_START] + + ; Read kernel blocks from CD/DVD +.read_loop: + push cx ; save block count + push ax ; save LBA low + push dx ; save LBA high + push es ; save buffer segment + + ; Set up DAP for INT 13h + mov [BDVD_DAP_BLK - BDVD_START], ax + mov [BDVD_DAP_BLK + 2 - BDVD_START], dx + mov ax, es + mov [BDVD_DAP_BUF + 2 - BDVD_START], ax ; segment + mov si, BDVD_DAP - BDVD_START ; DS:SI = DAP + mov ah, 0x42 + mov dl, [BDVD_BIOS_DRV_NUM - BDVD_START] + int 0x13 + + ; Advance buffer by one DVD block + pop ax ; ES + add ax, DVD_BLK_SIZE / 16 + mov es, ax + pop dx ; LBA high + pop ax ; LBA low + inc ax + jnz .no_carry + inc dx +.no_carry: + + ; Print progress dot + push ax + mov bx, [BDVD_PROGRESS_VAL + 2 - BDVD_START] + mov eax, [BDVD_PROGRESS_STEP - BDVD_START] + add [BDVD_PROGRESS_VAL - BDVD_START], eax + cmp [BDVD_PROGRESS_VAL + 2 - BDVD_START], bx + je .no_dot + mov al, '.' + call BDVD_PUT_CHAR +.no_dot: + pop ax + + pop cx + loop .read_loop + + ; Shift data backward to align on 512-byte boundary within DVD block + push ds + mov bx, [BDVD_SHIFT_BLKS - BDVD_START] + shl bx, BLK_SIZE_BITS - 4 ; Convert blocks to segment offset + mov cx, [BDVD_BLK_CNT - BDVD_START] + mov ax, BOOT_RAM_BASE / 16 + mov es, ax + add ax, bx + mov ds, ax +.shift_loop: + push cx + xor si, si + xor di, di + mov cx, DVD_BLK_SIZE / 4 + rep movsd + mov ax, ds + add ax, DVD_BLK_SIZE / 16 + mov ds, ax + mov ax, es + add ax, DVD_BLK_SIZE / 16 + mov es, ax + pop cx + loop .shift_loop + pop ds + + ; Pass boot info to kernel in registers + ; EBX = 32-bit LBA (BLK_LO in low word, BLK_HI in high word) + mov ebx, dword [BDVD_BLK_LO - BDVD_START] + ; EAX = SHIFT_BLKS in high word, BOOT_SRC_DVD in low word + mov ax, [BDVD_SHIFT_BLKS - BDVD_START] + shl eax, 16 + mov ax, BOOT_SRC_DVD + + ; Far jump to kernel entry point at BOOT_RAM_BASE + db 0xEA + dw 0x0000, BOOT_RAM_BASE / 16 + +BDVD_END: + +; Verify boot loader fits in DVD_BOOT_LOADER_SIZE +%if (BDVD_END - BDVD_START) > DVD_BOOT_LOADER_SIZE + %error "Boot loader exceeds DVD_BOOT_LOADER_SIZE" +%endif + +; Pad to exactly DVD_BOOT_LOADER_SIZE +times DVD_BOOT_LOADER_SIZE - (BDVD_END - BDVD_START) db 0 diff --git a/tools/boot_dvd.bin b/tools/boot_dvd.bin new file mode 100644 index 00000000..44b46e98 Binary files /dev/null and b/tools/boot_dvd.bin differ diff --git a/tools/boot_dvd.lst b/tools/boot_dvd.lst new file mode 100644 index 00000000..7588a296 --- /dev/null +++ b/tools/boot_dvd.lst @@ -0,0 +1,233 @@ + 1 ; boot_dvd.asm - NASM translation of TempleOS BootDVD.HC + 2 ; Boot loader for CD/DVD-ROM booting via El Torito + 3 ; Assembled with: nasm -f bin -o boot_dvd.bin boot_dvd.asm + 4 + 5 BITS 16 + 6 + 7 ; Constants from KernelA.HH + 8 %define BLK_SIZE_BITS 9 + 9 %define BLK_SIZE (1 << BLK_SIZE_BITS) ; 512 + 10 %define DVD_BLK_SIZE (4 * BLK_SIZE) ; 2048 + 11 %define DVD_BOOT_LOADER_SIZE DVD_BLK_SIZE ; 2048 + 12 %define BOOT_RAM_BASE 0x07C00 + 13 %define BOOT_RAM_LIMIT 0x97000 + 14 %define BOOT_STK_SIZE BLK_SIZE ; 512 + 15 %define BOOT_SRC_DVD 4 + 16 + 17 ; BOOT_HIGH_LOC = (BOOT_RAM_LIMIT - (BOOT_STK_SIZE + DVD_BOOT_LOADER_SIZE)) >> 4 + 18 ; = (0x97000 - (512 + 2048)) >> 4 = (0x97000 - 0xA00) >> 4 = 0x96600 >> 4 = 0x9660 + 19 %define BOOT_HIGH_LOC 0x9660 + 20 + 21 BDVD_START: + 22 00000000 FC cld + 23 00000001 B86096 mov ax, BOOT_HIGH_LOC + 24 00000004 8EC0 mov es, ax + 25 + 26 00000006 FA cli + 27 00000007 8ED0 mov ss, ax + 28 00000009 BC000A mov sp, BOOT_STK_SIZE + DVD_BOOT_LOADER_SIZE + 29 0000000C FB sti + 30 + 31 ; Determine our load address and copy ourselves to BOOT_HIGH_LOC + 32 0000000D E80000 call BDVD_GET_RIP + 33 BDVD_GET_RIP: + 34 00000010 5B pop bx + 35 00000011 83EB10 sub bx, BDVD_GET_RIP - BDVD_START + 36 00000014 C1EB04 shr bx, 4 + 37 00000017 8CC8 mov ax, cs + 38 00000019 01D8 add ax, bx + 39 0000001B 8ED8 mov ds, ax + 40 0000001D B90008 mov cx, DVD_BOOT_LOADER_SIZE + 41 00000020 31F6 xor si, si + 42 00000022 31FF xor di, di + 43 00000024 F3A4 rep movsb + 44 + 45 00000026 B86096 mov ax, BOOT_HIGH_LOC + 46 00000029 8ED8 mov ds, ax + 47 + 48 ; Far jump to relocated code at BOOT_HIGH_LOC:BDVD_MAIN + 49 0000002B EA db 0xEA + 50 0000002C A8006096 dw BDVD_MAIN - BDVD_START, BOOT_HIGH_LOC + 51 + 52 ; Data area + 53 00000030 00 BDVD_BIOS_DRV_NUM: db 0 + 54 00000031 00 BDVD_PAGE: db 0 + 55 + 56 ; BIOS Disk Address Packet for INT 13h/AH=42h + 57 00000032 1000 BDVD_DAP: db 16, 0 ; size=16, reserved=0 + 58 00000034 0100 db 1, 0 ; count=1, reserved=0 + 59 00000036 00000000 BDVD_DAP_BUF: dw 0, 0 ; offset, segment + 60 0000003A 0000000000000000 BDVD_DAP_BLK: dq 0 ; LBA + 61 + 62 00000042 4C6F6164696E672054- BDVD_TEMPLEOS_MSG: db "Loading TempleOS", 0 + 62 0000004B 656D706C654F5300 + 63 00000053 54656D706C654F5320- BDVD_NOT64_MSG: db "TempleOS requires a 64-bit capable processor.", 13, 10, 0 + 63 0000005C 726571756972657320- + 63 00000065 612036342D62697420- + 63 0000006E 63617061626C652070- + 63 00000077 726F636573736F722E- + 63 00000080 0D0A00 + 64 + 65 ; These fields get patched by build_iso.py with the kernel location + 66 ; Offsets from BDVD_START are used for patching + 67 00000083 0000 BDVD_BLK_LO: dw 0 ; DVD LBA low word + 68 00000085 0000 BDVD_BLK_HI: dw 0 ; DVD LBA high word + 69 00000087 0000 BDVD_BLK_CNT: dw 0 ; Number of DVD blocks to read + 70 00000089 0000 BDVD_SHIFT_BLKS: dw 0 ; Sub-block alignment offset + 71 0000008B 00000000 BDVD_PROGRESS_STEP: dd 0 + 72 0000008F 00000000 BDVD_PROGRESS_VAL: dd 0 + 73 + 74 ; Put a character to screen via BIOS + 75 BDVD_PUT_CHAR: + 76 00000093 B40E mov ah, 0x0E + 77 00000095 B307 mov bl, 7 + 78 00000097 8A3E3100 mov bh, [BDVD_PAGE - BDVD_START] + 79 0000009B CD10 int 0x10 + 80 BDVD_RET: + 81 0000009D C3 ret + 82 + 83 ; Print null-terminated string at DS:SI + 84 BDVD_PUTS: + 85 .loop: + 86 0000009E AC lodsb + 87 0000009F 84C0 test al, al + 88 000000A1 74FA jz BDVD_RET + 89 000000A3 E8EDFF call BDVD_PUT_CHAR + 90 000000A6 EBF6 jmp .loop + 91 + 92 ; Main entry point (runs after relocation) + 93 BDVD_MAIN: + 94 000000A8 88163000 mov [BDVD_BIOS_DRV_NUM - BDVD_START], dl + 95 + 96 ; Get current video page + 97 000000AC B40F mov ah, 0x0F + 98 000000AE CD10 int 0x10 + 99 000000B0 883E3100 mov [BDVD_PAGE - BDVD_START], bh + 100 + 101 ; Check for 64-bit CPU support + 102 000000B4 66B800000080 mov eax, 0x80000000 + 103 000000BA 0FA2 cpuid + 104 000000BC 663D01000080 cmp eax, 0x80000001 + 105 000000C2 720F jb .not64 + 106 + 107 000000C4 66B801000080 mov eax, 0x80000001 + 108 000000CA 0FA2 cpuid + 109 000000CC 660FBAE21D bt edx, 29 + 110 000000D1 7208 jc .is64 + 111 + 112 .not64: + 113 000000D3 BE5300 mov si, BDVD_NOT64_MSG - BDVD_START + 114 000000D6 E8C5FF call BDVD_PUTS + 115 .hang: + 116 000000D9 EBFE jmp .hang + 117 + 118 .is64: + 119 000000DB BE4200 mov si, BDVD_TEMPLEOS_MSG - BDVD_START + 120 000000DE E8BDFF call BDVD_PUTS + 121 + 122 ; Set up ES to point to kernel load area + 123 000000E1 B8C007 mov ax, BOOT_RAM_BASE / 16 + 124 000000E4 8EC0 mov es, ax + 125 000000E6 6631C9 xor ecx, ecx + 126 000000E9 8B0E8700 mov cx, [BDVD_BLK_CNT - BDVD_START] + 127 + 128 ; Calculate progress bar step + 129 000000ED 66B800004000 mov eax, (80 - 7 - 9) * 65536 ; 64 columns * 65536 + 130 000000F3 6631D2 xor edx, edx + 131 000000F6 66F7F1 div ecx + 132 000000F9 66A38B00 mov [BDVD_PROGRESS_STEP - BDVD_START], eax + 133 000000FD 66C7068F0000000000 mov dword [BDVD_PROGRESS_VAL - BDVD_START], 0 + 134 + 135 ; Load starting LBA + 136 00000106 A18300 mov ax, [BDVD_BLK_LO - BDVD_START] + 137 00000109 8B168500 mov dx, [BDVD_BLK_HI - BDVD_START] + 138 + 139 ; Read kernel blocks from CD/DVD + 140 .read_loop: + 141 0000010D 51 push cx ; save block count + 142 0000010E 50 push ax ; save LBA low + 143 0000010F 52 push dx ; save LBA high + 144 00000110 06 push es ; save buffer segment + 145 + 146 ; Set up DAP for INT 13h + 147 00000111 A33A00 mov [BDVD_DAP_BLK - BDVD_START], ax + 148 00000114 89163C00 mov [BDVD_DAP_BLK + 2 - BDVD_START], dx + 149 00000118 8CC0 mov ax, es + 150 0000011A A33800 mov [BDVD_DAP_BUF + 2 - BDVD_START], ax ; segment + 151 0000011D BE3200 mov si, BDVD_DAP - BDVD_START ; DS:SI = DAP + 152 00000120 B442 mov ah, 0x42 + 153 00000122 8A163000 mov dl, [BDVD_BIOS_DRV_NUM - BDVD_START] + 154 00000126 CD13 int 0x13 + 155 + 156 ; Advance buffer by one DVD block + 157 00000128 58 pop ax ; ES + 158 00000129 058000 add ax, DVD_BLK_SIZE / 16 + 159 0000012C 8EC0 mov es, ax + 160 0000012E 5A pop dx ; LBA high + 161 0000012F 58 pop ax ; LBA low + 162 00000130 40 inc ax + 163 00000131 7501 jnz .no_carry + 164 00000133 42 inc dx + 165 .no_carry: + 166 + 167 ; Print progress dot + 168 00000134 50 push ax + 169 00000135 8B1E9100 mov bx, [BDVD_PROGRESS_VAL + 2 - BDVD_START] + 170 00000139 66A18B00 mov eax, [BDVD_PROGRESS_STEP - BDVD_START] + 171 0000013D 6601068F00 add [BDVD_PROGRESS_VAL - BDVD_START], eax + 172 00000142 391E9100 cmp [BDVD_PROGRESS_VAL + 2 - BDVD_START], bx + 173 00000146 7405 je .no_dot + 174 00000148 B02E mov al, '.' + 175 0000014A E846FF call BDVD_PUT_CHAR + 176 .no_dot: + 177 0000014D 58 pop ax + 178 + 179 0000014E 59 pop cx + 180 0000014F E2BC loop .read_loop + 181 + 182 ; Shift data backward to align on 512-byte boundary within DVD block + 183 00000151 1E push ds + 184 00000152 8B1E8900 mov bx, [BDVD_SHIFT_BLKS - BDVD_START] + 185 00000156 C1E305 shl bx, BLK_SIZE_BITS - 4 ; Convert blocks to segment offset + 186 00000159 8B0E8700 mov cx, [BDVD_BLK_CNT - BDVD_START] + 187 0000015D B8C007 mov ax, BOOT_RAM_BASE / 16 + 188 00000160 8EC0 mov es, ax + 189 00000162 01D8 add ax, bx + 190 00000164 8ED8 mov ds, ax + 191 .shift_loop: + 192 00000166 51 push cx + 193 00000167 31F6 xor si, si + 194 00000169 31FF xor di, di + 195 0000016B B90002 mov cx, DVD_BLK_SIZE / 4 + 196 0000016E 66F3A5 rep movsd + 197 00000171 8CD8 mov ax, ds + 198 00000173 058000 add ax, DVD_BLK_SIZE / 16 + 199 00000176 8ED8 mov ds, ax + 200 00000178 8CC0 mov ax, es + 201 0000017A 058000 add ax, DVD_BLK_SIZE / 16 + 202 0000017D 8EC0 mov es, ax + 203 0000017F 59 pop cx + 204 00000180 E2E4 loop .shift_loop + 205 00000182 1F pop ds + 206 + 207 ; Pass boot info to kernel in registers + 208 ; EBX = 32-bit LBA (BLK_LO in low word, BLK_HI in high word) + 209 00000183 668B1E8300 mov ebx, dword [BDVD_BLK_LO - BDVD_START] + 210 ; EAX = SHIFT_BLKS in high word, BOOT_SRC_DVD in low word + 211 00000188 A18900 mov ax, [BDVD_SHIFT_BLKS - BDVD_START] + 212 0000018B 66C1E010 shl eax, 16 + 213 0000018F B80400 mov ax, BOOT_SRC_DVD + 214 + 215 ; Far jump to kernel entry point at BOOT_RAM_BASE + 216 00000192 EA db 0xEA + 217 00000193 0000C007 dw 0x0000, BOOT_RAM_BASE / 16 + 218 + 219 BDVD_END: + 220 + 221 ; Verify boot loader fits in DVD_BOOT_LOADER_SIZE + 222 %if (BDVD_END - BDVD_START) > DVD_BOOT_LOADER_SIZE + 223 %error "Boot loader exceeds DVD_BOOT_LOADER_SIZE" + 224 %endif + 225 + 226 ; Pad to exactly DVD_BOOT_LOADER_SIZE + 227 00000197 00 times DVD_BOOT_LOADER_SIZE - (BDVD_END - BDVD_START) db 0 diff --git a/tools/build_iso.py b/tools/build_iso.py new file mode 100644 index 00000000..ffc2deb9 --- /dev/null +++ b/tools/build_iso.py @@ -0,0 +1,602 @@ +#!/usr/bin/env python3 +"""Build a bootable TempleOS ISO from the source tree. + +Creates a RedSea filesystem image wrapped in ISO 9660 with El Torito boot +headers. The precompiled kernel binary (0000Kernel.BIN.C) is embedded and +the boot loader is patched with its LBA. + +Usage: + python3 build_iso.py [source_dir] [output.iso] +""" + +import os +import struct +import sys +import time +from pathlib import Path + +# TempleOS constants +BLK_SIZE_BITS = 9 +BLK_SIZE = 1 << BLK_SIZE_BITS # 512 +DVD_BLK_SIZE = 4 * BLK_SIZE # 2048 +DVD_BOOT_LOADER_SIZE = DVD_BLK_SIZE # 2048 + +# RedSea filesystem constants +CDIR_SIZE = 64 +CDIR_FILENAME_LEN = 38 +MBR_PT_REDSEA = 0x88 + +# RedSea attributes +RS_ATTR_READ_ONLY = 0x01 +RS_ATTR_HIDDEN = 0x02 +RS_ATTR_SYSTEM = 0x04 +RS_ATTR_VOL_ID = 0x08 +RS_ATTR_DIR = 0x10 +RS_ATTR_ARCHIVE = 0x20 +RS_ATTR_CONTIGUOUS = 0x800 + +# ISO 9660 types +ISOT_BOOT_RECORD = 0 +ISOT_PRI_VOL_DESC = 1 +ISOT_SUPPLEMENTARY_DESC = 2 +ISOT_TERMINATOR = 255 + +# Boot loader patch offsets (from NASM assembly) +# These are the offsets of the patchable fields within boot_dvd.bin +BDVD_BLK_LO_OFFSET = 0x83 +BDVD_BLK_HI_OFFSET = 0x85 +BDVD_BLK_CNT_OFFSET = 0x87 +BDVD_SHIFT_BLKS_OFFSET = 0x89 + + +def ceil_to(value: int, alignment: int) -> int: + return (value + alignment - 1) // alignment * alignment + + +def ceil_div(a: int, b: int) -> int: + return (a + b - 1) // b + + +def make_palindrome_u16(value: int) -> bytes: + """TempleOS both-endian U16 (big-endian first, little-endian second).""" + be = struct.pack(">H", value & 0xFFFF) + le = struct.pack(" bytes: + """TempleOS both-endian U32 (big-endian first, little-endian second).""" + be = struct.pack(">I", value & 0xFFFFFFFF) + le = struct.pack(" bytes: + """Create a 64-byte RedSea directory entry.""" + name_bytes = name.encode("ascii")[:CDIR_FILENAME_LEN - 1] + name_bytes = name_bytes + b"\x00" * (CDIR_FILENAME_LEN - len(name_bytes)) + return struct.pack( + " None: + self.host_path = host_path + self.name = name + self.size = size + self.blocks = ceil_div(size, BLK_SIZE) if size > 0 else 0 + self.clus = 0 # assigned during layout + + +class DirEntry: + """Represents a directory in the RedSea filesystem.""" + + def __init__(self, name: str) -> None: + self.name = name + self.files: list[FileEntry] = [] + self.subdirs: list["DirEntry"] = [] + self.clus = 0 # assigned during layout + self.size = 0 # in bytes, assigned during layout + self.blocks = 0 # assigned during layout + + +def scan_directory(host_path: str, name: str) -> DirEntry: + """Recursively scan a host directory into our tree structure.""" + d = DirEntry(name) + + try: + entries = sorted(os.listdir(host_path)) + except PermissionError: + return d + + for entry_name in entries: + full_path = os.path.join(host_path, entry_name) + + # Skip hidden files, .git, our tools directory, and generated ISOs + if (entry_name.startswith(".") + or entry_name == "tools" + or entry_name.endswith(".iso")): + continue + + # TempleOS filenames are max 37 chars + if len(entry_name) > CDIR_FILENAME_LEN - 1: + print(f" Warning: skipping '{entry_name}' (name too long)") + continue + + if os.path.isdir(full_path): + subdir = scan_directory(full_path, entry_name) + d.subdirs.append(subdir) + elif os.path.isfile(full_path): + size = os.path.getsize(full_path) + d.files.append(FileEntry(full_path, entry_name, size)) + + return d + + +def compute_dir_size(d: DirEntry) -> None: + """Compute directory entry block sizes recursively.""" + # Each directory has: ".", "..", entries..., null terminator + entry_count = 3 + len(d.files) + len(d.subdirs) + d.size = ceil_to(entry_count * CDIR_SIZE, BLK_SIZE) + d.blocks = d.size // BLK_SIZE + + for subdir in d.subdirs: + compute_dir_size(subdir) + + +def count_data_blocks(d: DirEntry) -> int: + """Count total data blocks needed for a directory tree.""" + total = d.blocks # this directory's entries + + for f in d.files: + total += f.blocks + + for subdir in d.subdirs: + total += count_data_blocks(subdir) + + return total + + +def assign_clusters( + d: DirEntry, + next_clus: int, + parent_clus: int, +) -> int: + """Assign cluster (block) numbers to all entries. Returns next free cluster.""" + d.clus = next_clus + next_clus += d.blocks + + for f in d.files: + if f.blocks > 0: + f.clus = next_clus + next_clus += f.blocks + else: + f.clus = 0 + + for subdir in d.subdirs: + next_clus = assign_clusters(subdir, next_clus, d.clus) + + return next_clus + + +def build_dir_data(d: DirEntry, parent_clus: int) -> bytes: + """Build the raw bytes for a directory's entry table.""" + entries = bytearray() + + # "." entry + entries += make_dir_entry( + ".", RS_ATTR_DIR | RS_ATTR_CONTIGUOUS, d.clus, d.size + ) + + # ".." entry + entries += make_dir_entry( + "..", RS_ATTR_DIR | RS_ATTR_CONTIGUOUS, parent_clus, 0 + ) + + # File entries + for f in d.files: + attr = RS_ATTR_CONTIGUOUS + entries += make_dir_entry(f.name, attr, f.clus, f.size) + + # Subdirectory entries + for subdir in d.subdirs: + entries += make_dir_entry( + subdir.name, + RS_ATTR_DIR | RS_ATTR_CONTIGUOUS, + subdir.clus, + subdir.size, + ) + + # Pad to directory size (null terminator entry is implicit - zeroed padding) + entries += b"\x00" * (d.size - len(entries)) + + return bytes(entries) + + +def write_dir_tree( + iso: bytearray, + d: DirEntry, + parent_clus: int, +) -> None: + """Recursively write directory entries and file data into the ISO image.""" + # Write this directory's entries + dir_data = build_dir_data(d, parent_clus) + offset = d.clus * BLK_SIZE + iso[offset : offset + len(dir_data)] = dir_data + + # Write file contents + for f in d.files: + if f.size > 0: + with open(f.host_path, "rb") as fh: + data = fh.read() + offset = f.clus * BLK_SIZE + iso[offset : offset + f.size] = data + + # Recurse into subdirectories + for subdir in d.subdirs: + write_dir_tree(iso, subdir, d.clus) + + +def build_bitmap( + data_area: int, + total_data_blocks: int, + bitmap_blks: int, +) -> bytearray: + """Build the allocation bitmap. Bit N = block (data_area + N) is allocated.""" + bitmap_size = bitmap_blks * BLK_SIZE + bitmap = bytearray(bitmap_size) + + # Mark all used data blocks as allocated + # Data blocks run from data_area to data_area + total_data_blocks - 1 + for i in range(total_data_blocks): + byte_idx = i // 8 + bit_idx = i % 8 + if byte_idx < bitmap_size: + bitmap[byte_idx] |= 1 << bit_idx + + return bitmap + + +def build_redsea_boot_sector( + drv_offset: int, + sects: int, + root_clus: int, + bitmap_sects: int, +) -> bytes: + """Build the 512-byte RedSea boot sector (CRedSeaBoot).""" + sector = bytearray(BLK_SIZE) + + # jump_and_nop[3] - short jump past header + nop + sector[0] = 0xEB + sector[1] = 0x1E # jump +30 + sector[2] = 0x07 # nop-equivalent (POP ES in real mode) + + # signature (offset 3) + sector[3] = MBR_PT_REDSEA # 0x88 + + # reserved[4] (offset 4-7) + # Leave as zeros + + # drv_offset (I64, offset 8) + struct.pack_into(" tuple[bytes, bytes, bytes, bytes, bytes]: + """Build ISO 9660 descriptors and El Torito catalog. + + Returns (pvd, boot_record, svd, terminator, el_torito_catalog). + Each is DVD_BLK_SIZE (2048) bytes. + """ + # Primary Volume Descriptor + # Field offsets match TempleOS CISOPriDesc struct, which differs from + # ISO 9660 standard after offset 132 (pad[20] instead of pad[24]). + pvd = bytearray(DVD_BLK_SIZE) + pvd[0] = ISOT_PRI_VOL_DESC # type (offset 0) + pvd[1:6] = b"CD001" # id (offset 1) + pvd[6] = 1 # version (offset 6) + # vol_space_size at offset 80 (CPalindromeU32) + pvd[80:88] = make_palindrome_u32(vol_space_size_dvd) + # vol_set_size at offset 120 (CPalindromeU16) + pvd[120:124] = make_palindrome_u16(1) + # vol_seq_num at offset 124 (CPalindromeU16) + pvd[124:128] = make_palindrome_u16(1) + # log_block_size at offset 128 (CPalindromeU16) + pvd[128:132] = make_palindrome_u16(DVD_BLK_SIZE) + # root_dir_record at offset 152 (CISODirEntry: pad[2] + CPalindromeU32 loc) + # loc is at offset 154 + pvd[154:162] = make_palindrome_u32(root_clus) + # publisher_id at offset 314 (TempleOS struct offset, NOT ISO 9660's 318) + pub_id = b"TempleOS RedSea" + pvd[314 : 314 + len(pub_id)] = pub_id + # file_structure_version at offset 877 (TempleOS struct offset) + pvd[877] = 1 + + # Boot Record (El Torito) + boot_rec = bytearray(DVD_BLK_SIZE) + boot_rec[0] = ISOT_BOOT_RECORD + boot_rec[1:6] = b"CD001" + boot_rec[6] = 1 + boot_rec[7 : 7 + len(b"EL TORITO SPECIFICATION")] = ( + b"EL TORITO SPECIFICATION" + ) + # Pointer to El Torito catalog at offset 0x47 (71) + # Catalog is at DVD sector 20 + struct.pack_into(" FileEntry | None: + """Find the kernel binary in the directory tree.""" + for subdir in root.subdirs: + if subdir.name == "0000Boot": + for f in subdir.files: + if f.name == "0000Kernel.BIN.C": + return f + return None + + +def patch_boot_loader( + boot_loader: bytearray, + kernel_clus: int, + kernel_size: int, +) -> None: + """Patch the boot loader with kernel location on the ISO. + + The boot loader reads DVD blocks (2048 bytes). We convert the kernel's + 512-byte block address to a DVD LBA. + """ + dvd_lba = kernel_clus >> 2 + dvd_blk_cnt = ceil_div(kernel_size, DVD_BLK_SIZE) + shift_blks = kernel_clus & 3 + + if shift_blks: + dvd_blk_cnt += 1 + + struct.pack_into("> 16) & 0xFFFF + ) + struct.pack_into(" None: + tools_dir = os.path.dirname(os.path.abspath(__file__)) + boot_loader_path = os.path.join(tools_dir, "boot_dvd.bin") + + if not os.path.exists(boot_loader_path): + print("Error: boot_dvd.bin not found. Assemble it first:") + print(" nasm -f bin -o tools/boot_dvd.bin tools/boot_dvd.asm") + sys.exit(1) + + with open(boot_loader_path, "rb") as f: + boot_loader = bytearray(f.read()) + + print(f"Scanning source directory: {source_dir}") + root = scan_directory(source_dir, "/") + + # Compute directory sizes + compute_dir_size(root) + + # Count total data blocks needed + data_blocks = count_data_blocks(root) + print(f" Files/dirs need {data_blocks} blocks ({data_blocks * BLK_SIZE} bytes)") + + # ISO layout: 0-15 system area, 16 PVD, 17 boot rec, 18 SVD, 19 term, + # 20 El Torito catalog, 21 boot loader + # drv_offset = 19*4 + (DVD_BLK_SIZE*2 + DVD_BOOT_LOADER_SIZE) / BLK_SIZE + drv_offset = 19 * 4 + (DVD_BLK_SIZE * 2 + DVD_BOOT_LOADER_SIZE) // BLK_SIZE + # = 76 + 12 = 88 + + # Calculate bitmap blocks iteratively + # bitmap must track: data_blocks + bitmap_blks (since bitmap is in data area) + bitmap_blks = 1 + while True: + new_bitmap_blks = ceil_div(data_blocks + bitmap_blks, BLK_SIZE * 8) + if new_bitmap_blks == bitmap_blks: + break + bitmap_blks = new_bitmap_blks + + # data_area = drv_offset + bitmap_blks + data_area = drv_offset + bitmap_blks + + # Total data includes: bitmap_blks (self-tracked) + actual data + total_data_in_area = bitmap_blks + data_blocks + + # max_blk (inclusive, aligned to 4-block DVD boundary) + max_blk = ceil_to(drv_offset + 1 + bitmap_blks + data_blocks, 4) - 1 + sects = max_blk - drv_offset + 1 + + total_iso_blocks = max_blk + 1 + total_iso_bytes = total_iso_blocks * BLK_SIZE + + print(f" drv_offset={drv_offset}, bitmap_blks={bitmap_blks}, " + f"data_area={data_area}") + print(f" sects={sects}, max_blk={max_blk}") + print(f" ISO size: {total_iso_bytes} bytes " + f"({total_iso_bytes / (1024 * 1024):.1f} MB)") + + # Assign clusters to all files and directories + # First data block after bitmap self-allocation + first_data_clus = data_area + bitmap_blks # skip bitmap's own blocks + # Actually: data_area is where data starts, and the first bitmap_blks + # blocks in the data area are the bitmap itself. So real data starts at + # data_area + bitmap_blks? No... + # + # Per TempleOS: data_area = drv_offset + bitmap_sects. The bitmap lives at + # blocks [drv_offset+1, drv_offset+bitmap_sects]. The first data block is + # at data_area (= drv_offset + bitmap_sects). + # + # In RedSeaFmt, ClusAlloc(0,1,FALSE) allocates the first data block (at + # data_area). Then root_clus gets the next allocation. + # + # But the "Alloc #1" block at data_area overlaps with the last bitmap block. + # For our purposes, we skip 1 block (the reserved/overlap block) and start + # real data at data_area + 1. + + first_data_clus = data_area + 1 # skip the reserved "Alloc #1" block + next_clus = assign_clusters(root, first_data_clus, first_data_clus) + + # Find kernel binary for boot loader patching + kernel_entry = find_kernel_entry(root) + if kernel_entry is None: + print("Error: Could not find 0000Boot/0000Kernel.BIN.C in source tree") + sys.exit(1) + + print(f" Kernel at cluster {kernel_entry.clus} " + f"({kernel_entry.size} bytes, {kernel_entry.blocks} blocks)") + + # Patch boot loader + patch_boot_loader(boot_loader, kernel_entry.clus, kernel_entry.size) + + # Build the ISO image + print("Building ISO image...") + iso = bytearray(total_iso_bytes) + + # Write boot loader at DVD sector 21 (512-byte block 84) + boot_loader_block = 21 * 4 # = 84 + iso[ + boot_loader_block * BLK_SIZE : boot_loader_block * BLK_SIZE + + DVD_BOOT_LOADER_SIZE + ] = boot_loader + + # Write RedSea boot sector + boot_sector = build_redsea_boot_sector( + drv_offset, sects, root.clus, bitmap_blks + ) + iso[drv_offset * BLK_SIZE : drv_offset * BLK_SIZE + BLK_SIZE] = boot_sector + + # Build and write allocation bitmap + # Bitmap tracks from data_area onwards. We need to mark: + # - Block at data_area (the reserved "Alloc #1" block) + # - All file/directory data blocks + bitmap = build_bitmap(data_area, next_clus - data_area, bitmap_blks) + bitmap_offset = (drv_offset + 1) * BLK_SIZE + iso[bitmap_offset : bitmap_offset + len(bitmap)] = bitmap + + # Write directory tree and file data + write_dir_tree(iso, root, root.clus) + + # Write ISO 9660 headers + vol_space_size_dvd = total_iso_bytes // DVD_BLK_SIZE + pvd, boot_rec, svd, term, et_catalog = build_iso9660_headers( + vol_space_size_dvd, root.clus + ) + iso[16 * DVD_BLK_SIZE : 17 * DVD_BLK_SIZE] = pvd + iso[17 * DVD_BLK_SIZE : 18 * DVD_BLK_SIZE] = boot_rec + iso[18 * DVD_BLK_SIZE : 19 * DVD_BLK_SIZE] = svd + iso[19 * DVD_BLK_SIZE : 20 * DVD_BLK_SIZE] = term + iso[20 * DVD_BLK_SIZE : 21 * DVD_BLK_SIZE] = et_catalog + + # Write output + print(f"Writing {output_path}...") + with open(output_path, "wb") as f: + f.write(iso) + + print(f"Done! ISO written to {output_path}") + print(f"Boot with: qemu-system-x86_64 -cdrom {output_path} -m 512") + + +def main() -> None: + if len(sys.argv) < 2: + # Default: source is parent of tools/ dir + source_dir = os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + ) + else: + source_dir = sys.argv[1] + + if len(sys.argv) < 3: + output_path = os.path.join(source_dir, "TempleOS.iso") + else: + output_path = sys.argv[2] + + build_iso(source_dir, output_path) + + +if __name__ == "__main__": + main() diff --git a/tools/run_qemu.sh b/tools/run_qemu.sh new file mode 100755 index 00000000..56558375 --- /dev/null +++ b/tools/run_qemu.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Launch TempleOS in QEMU +# Usage: ./run_qemu.sh [path/to/TempleOS.iso] + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +ISO="${1:-$REPO_DIR/TempleOS.iso}" + +if [ ! -f "$ISO" ]; then + echo "ISO not found: $ISO" + echo "Build it first: python3 $SCRIPT_DIR/build_iso.py" + exit 1 +fi + +# Try HVF acceleration on macOS, fall back to TCG +if qemu-system-x86_64 -accel help 2>&1 | grep -q hvf; then + ACCEL="-machine accel=hvf" +else + ACCEL="" +fi + +qemu-system-x86_64 \ + -cdrom "$ISO" \ + -m 512 \ + $ACCEL \ + -serial stdio \ + "$@"