A browser+player for place.stream.video records
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

VodBrowser Implementation

stavola.xyz 81df58c8

+10380
+43
.gitignore
··· 1 + # Object files and static libraries 2 + *.o 3 + *.a 4 + 5 + # Built .prg programs — regenerable from source via `make` / `./deploy.sh`. 6 + *.prg 7 + 8 + # Build trees for the two ELF-based subprograms. All contents are 9 + # intermediate objects, linker output, and patch-applied sentinels. 10 + media/build_elf/ 11 + net/build_elf/ 12 + 13 + # Generated ISO — keep the isos/ folder itself, ignore its contents. 14 + isos/* 15 + !isos/.gitkeep 16 + 17 + # QEMU virtual disks (created by qemu-run.sh) and any other 18 + # ephemeral VM images that accumulate from running the local 19 + # build / test scripts. 20 + *.qcow2 21 + 22 + # Python bytecode caches from tools/ imports (tools/mkdistroiso.py 23 + # is imported as a module during debugging). 24 + __pycache__/ 25 + *.pyc 26 + 27 + # Tool binaries 28 + tools/mkredseaiso 29 + 30 + # Third-party library build outputs 31 + lib/bearssl/build/ 32 + lib/openh264/*.a 33 + lib/openh264/*.dylib 34 + lib/openh264/*.o 35 + lib/openh264/**/*.o 36 + lib/openh264/**/*.d 37 + lib/media-server/**/debug.* 38 + lib/media-server/**/*.o 39 + lib/media-server/**/*.d 40 + lib/media-server/**/*.a 41 + 42 + # Claude Code per-project state 43 + .claude/
+18
.gitmodules
··· 1 + [submodule "lib/bearssl"] 2 + path = lib/bearssl 3 + url = https://www.bearssl.org/git/BearSSL 4 + [submodule "lib/jsmn"] 5 + path = lib/jsmn 6 + url = https://github.com/zserge/jsmn.git 7 + [submodule "lib/lwip"] 8 + path = lib/lwip 9 + url = https://github.com/lwip-tcpip/lwip.git 10 + [submodule "lib/media-server"] 11 + path = lib/media-server 12 + url = https://github.com/ireader/media-server.git 13 + [submodule "lib/openh264"] 14 + path = lib/openh264 15 + url = https://github.com/cisco/openh264.git 16 + [submodule "lib/picohttpparser"] 17 + path = lib/picohttpparser 18 + url = https://github.com/h2o/picohttpparser.git
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 Matt Stavola 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+39
Makefile
··· 1 + # VodBrowser top-level Makefile. 2 + # 3 + # Most of the time you want `./deploy.sh` instead — it builds the two 4 + # .prg files, stamps Version.HC, and packages the RedSea ISO for UTM. 5 + # 6 + # Targets here are for building the .prg files standalone when you 7 + # don't want to repackage the ISO. 8 + 9 + .PHONY: all net media clean distclean 10 + 11 + # Pre-built dependency libraries 12 + BEARSSL_LIB = lib/bearssl/build/libbearssl.a 13 + OPENH264_LIB = lib/openh264/libopenh264.a 14 + 15 + all: net media 16 + 17 + # ---- Dependency libraries (built once) ---- 18 + 19 + $(BEARSSL_LIB): 20 + $(MAKE) -C lib/bearssl 21 + 22 + $(OPENH264_LIB): 23 + $(MAKE) -C lib/openh264 libraries 24 + 25 + # ---- TempleOS .prg targets ---- 26 + 27 + net: $(BEARSSL_LIB) 28 + $(MAKE) -C net elf 29 + 30 + media: $(OPENH264_LIB) 31 + $(MAKE) -C media elf 32 + 33 + clean: 34 + $(MAKE) -C net clean 35 + $(MAKE) -C media clean 36 + 37 + distclean: clean 38 + $(MAKE) -C lib/bearssl clean || true 39 + $(MAKE) -C lib/openh264 clean || true
+101
README.md
··· 1 + # VodBrowser 2 + 3 + ![screenshot](screenshot.png) 4 + ![screenshot](screenshot2.png) 5 + ![screenshot](screenshot3.png) 6 + 7 + A browser for place.stream.video records with basic playback functionality, for TempleOS. Streams and plays H.264 video over HLS from place.stream.video records. Audio, pause/play, seeking, and thumbnails are not supported. 8 + 9 + This is my entry for [VODJAM](https://blog.stream.place/3micfu6ifyk2a). I like doing weird, dumb projects so when I looked at the "What we're looking for section" and saw "Hilarious and amazing nonsense that we couldn't possibly have predicted," it was a done deal. I had originally planned to finish this by Easter Sunday (he has risen, et cetera et cetera) but, believe it or not, this was a bit hard to get going properly and I totally blew past that artificial deadline. 10 + 11 + Now, after submission, I can safely say that I will definitely not be adding anything else to this repo. HolyC was nice to work with and TempleOS could be pleasant at times, but I spent way too much time chasing pointer addresses and reading asm to form good memories here. If you would like to contribute, I would suggest forking! 12 + 13 + ## Usage 14 + 15 + Using the prebuilt ISO, you can just invoke `VodBrowser` from the TempleOS REPL. 16 + 17 + ## Installation 18 + 19 + ### Prebuilt, all-in-one ISO 20 + 21 + [Download here](https://skywell.dev/file/HohEjf). Boot it in any TempleOS-compatible VMM and `VodBrowser;` is ready from the REPL. 22 + 23 + ### From source 24 + 25 + #### Prerequisites 26 + 27 + - [Zig](https://ziglang.org/) - used as the cross-compiler (`zig cc` / `zig c++`) targeting `x86_64-linux-musl`. Any recent Zig should work. 28 + - Python 3 - runs `elf/elf2bin.py`, `elf/make_program.py`, `tools/mkredseaiso.py`, and `tools/mkdistroiso.py` 29 + - `curl` - to fetch the upstream TempleOS ISO. You can skip this by dropping a `TempleOS.iso` into the project root manually. 30 + - [UTM](https://mac.getutm.app/) on macOS, QEMU on Linux, or any other VMM that can mount an ISO as a CD. 31 + 32 + #### Steps 33 + 34 + - Clone (with all submodules): 35 + 36 + ``` 37 + git clone --recursive <repo-url> templeat 38 + cd templeat 39 + ```` 40 + 41 + - Run `./deploy.sh` 42 + 43 + - Boot TempleOS in your VMM of choice and attach isos/sources.iso as a CD-ROM. 44 + 45 + - From the TempleOS REPL, load the sources and run it: 46 + 47 + ``` 48 + Cd("T:/"); 49 + #include "VodBrowser"; 50 + VodBrowser; 51 + ``` 52 + 53 + ## How it works 54 + 55 + The general approach is the one James Whitham describes in [Porting third-party programs to TempleOS](https://www.jwhitham.org/2015/07/porting-third-party-programs-to-templeos.html): take third-party C code, cross-compile it to a freestanding ELF that runs in TempleOS's address space, and bridge it back to HolyC through small trampolines. Almost all of the work here went into making specific C libraries tolerate the constraints of running under TempleOS. 56 + 57 + The rule of thumb for picking dependencies was to find the smallest, most self-contained C implementation of a thing and then wrap it. Every library under `lib/` is there because it had a minimal-dependency build that could survive without libc, without threads, and without hardware floating point. Anything that wanted libm, pthreads, or SSE had to be patched, stubbed, or swapped out for something simpler. 58 + 59 + ## Credits 60 + 61 + - **James Whitham**, whose writeup at [www.jwhitham.org/2015/07/porting-third-party-programs-to-templeos.html](https://www.jwhitham.org/2015/07/porting-third-party-programs-to-templeos.html) was the basis of the HolyC-to-C bridging in this project. 62 + 63 + - **[@iame.li](https://bsky.app/profile/iame.li)**, for building and running [stream.place](https://stream.place/) and hosting the VODJAM. 64 + 65 + ## Libraries 66 + 67 + Everything under `lib/` is a git submodule. Each one was picked for being the smallest self-contained implementation of its role that was permissively licensed and required minimal alterations, if any. 68 + 69 + | Library | Role | 70 + |---|---| 71 + | [`lib/bearssl`](https://bearssl.org/) | TLS for HTTPS (ATProto API, VOD segment fetches) | 72 + | [`lib/jsmn`](https://github.com/zserge/jsmn) | JSON parsing for ATProto API responses | 73 + | [`lib/lwip`](https://savannah.nongnu.org/projects/lwip/) | TCP/IP and DHCP, driven by a HolyC NIC shim | 74 + | [`lib/media-server`](https://github.com/ireader/media-server) | libmov, the fragmented MP4 demuxer | 75 + | [`lib/openh264`](https://www.openh264.org/) | H.264 / AVC video decoder | 76 + | [`lib/picohttpparser`](https://github.com/h2o/picohttpparser) | HTTP response header parsing | 77 + 78 + ## Repo layout 79 + 80 + holyc/ HolyC source. Entry point: VodBrowser.HC. 81 + net/ C source for Net.prg (lwIP, BearSSL, HTTP, JSON). 82 + media/ C source for Media.prg (libmov, openh264). 83 + elf/ Linker scripts and ELF-to-.prg Python tooling. 84 + lib/ Third-party submodules. 85 + tools/ RedSea ISO packer. 86 + isos/ Build output. 87 + 88 + Makefile Rebuild Net.prg and Media.prg. 89 + deploy.sh Build everything and pack sources.iso. 90 + qemu-run.sh Cross-platform QEMU launcher. 91 + build-vodbrowser-dist.sh Produce a bootable distributable ISO 92 + by injecting VodBrowser into the stock 93 + TempleOS ISO via tools/mkdistroiso.py. 94 + 95 + ## License 96 + 97 + [MIT](LICENSE) 98 + 99 + ## AI Usage 100 + 101 + Co-designed with Claude Opus, which did most of the low-level details.
+137
build-vodbrowser-dist.sh
··· 1 + #!/bin/bash 2 + # build-vodbrowser-dist.sh — produce isos/vodbrowser-dist.iso, a 3 + # bootable TempleOS ISO with VodBrowser pre-installed and set to 4 + # auto-launch on login. 5 + # 6 + # The heavy lifting is in tools/mkdistroiso.py: 7 + # 8 + # 1. Parse isos/TempleOS.iso (RedSea filesystem) into an in-memory 9 + # directory tree. 10 + # 2. Add /VodBrowser/ with every source .HC and binary .prg from 11 + # the deploy list. 12 + # 3. Drop a plain /HomeSys.HC at the root level so TempleOS's 13 + # login script launches VodBrowser on every boot. 14 + # 4. Re-serialise the tree into a new bootable ISO. 15 + # 16 + # No QEMU or VM loop involved — this runs entirely on the host and 17 + # produces a real CD-ROM image (.iso) in a few seconds. Mount it as 18 + # the boot CD in any emulator or burn it to physical media and boot 19 + # straight into VodBrowser. 20 + 21 + set -e 22 + 23 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 24 + ISO_DIR="$SCRIPT_DIR/isos" 25 + TOS_ISO="$ISO_DIR/TempleOS.iso" 26 + OUT_ISO="$ISO_DIR/vodbrowser-dist.iso" 27 + 28 + # ---- Preflight ---- 29 + 30 + if [ ! -f "$TOS_ISO" ]; then 31 + echo "ERROR: base TempleOS ISO missing: $TOS_ISO" 32 + echo "Download it from https://templeos.org/Downloads/TOS_Distro.ISO" 33 + echo "and drop it at $TOS_ISO before re-running." 34 + exit 1 35 + fi 36 + 37 + # Rebuild Net.prg / Media.prg if the build tree isn't current — 38 + # deploy.sh is idempotent so calling it here is cheap when nothing 39 + # has changed. Also re-stamps Version.HC with the current timestamp 40 + # so the dist ISO carries a build tag. 41 + echo "=== Refreshing Net.prg + Media.prg ===" 42 + "$SCRIPT_DIR/deploy.sh" >/dev/null 43 + echo " (deploy.sh completed; sources.iso also refreshed as a side effect)" 44 + 45 + # ---- Build the dist ISO ---- 46 + 47 + # Keep the file list in the exact order deploy.sh uses so both ISOs 48 + # land on the same canonical basename set. Anything we add to 49 + # deploy.sh later should also be added here. 50 + HC_FILES=( 51 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Load.HC" 52 + "$SCRIPT_DIR/holyc/vodbrowser/lib/TosCallbacks.HC" 53 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Nic.HC" 54 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Net.HC" 55 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Media.HC" 56 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Version.HC" 57 + "$SCRIPT_DIR/holyc/vodbrowser/Util.HC" 58 + "$SCRIPT_DIR/holyc/vodbrowser/Prefs.HC" 59 + "$SCRIPT_DIR/holyc/vodbrowser/NetTask.HC" 60 + "$SCRIPT_DIR/holyc/vodbrowser/DecodeTask.HC" 61 + "$SCRIPT_DIR/holyc/vodbrowser/VideoList.HC" 62 + "$SCRIPT_DIR/holyc/vodbrowser/Playback.HC" 63 + "$SCRIPT_DIR/holyc/vodbrowser/Settings.HC" 64 + "$SCRIPT_DIR/holyc/vodbrowser/Menu.HC" 65 + "$SCRIPT_DIR/holyc/VodBrowser.HC" 66 + ) 67 + PRG_FILES=( 68 + "$SCRIPT_DIR/net/Net.prg" 69 + "$SCRIPT_DIR/media/Media.prg" 70 + ) 71 + 72 + echo "" 73 + echo "=== Building $OUT_ISO ===" 74 + python3 "$SCRIPT_DIR/tools/mkdistroiso.py" \ 75 + --base "$TOS_ISO" \ 76 + --output "$OUT_ISO" \ 77 + "${HC_FILES[@]}" "${PRG_FILES[@]}" 78 + 79 + OUT_SIZE=$(wc -c < "$OUT_ISO" | tr -d ' ') 80 + echo "" 81 + echo "=== Done ===" 82 + echo " ISO: $OUT_ISO (${OUT_SIZE} bytes)" 83 + echo "" 84 + 85 + # Unless the caller passed --no-run, boot the freshly-built ISO in 86 + # QEMU so they can watch it come up. This uses the same device / 87 + # drive / accel layout as qemu-run.sh — cirrus VGA, pcnet NIC, 88 + # pc-i440fx-10.0, TCG with 512 MB translation block cache, 4-core 89 + # Skylake-Client, 2 GB RAM — so there's exactly one known-good 90 + # hardware profile for VodBrowser and the two scripts stay in sync. 91 + if [ "${1:-}" = "--no-run" ]; then 92 + echo "Skipping run (--no-run). Boot it yourself with:" 93 + echo " ./build-vodbrowser-dist.sh # rebuild" 94 + echo " (then paste the ISO into any TempleOS-capable emulator)" 95 + exit 0 96 + fi 97 + 98 + # Pick a display backend QEMU has compiled in. Homebrew QEMU on 99 + # macOS ships with cocoa but no sdl/gtk; most Linux QEMU builds 100 + # have gtk and sdl but no cocoa. Same probe qemu-run.sh uses. 101 + DISPLAY_BACKEND="" 102 + AVAILABLE_DISPLAYS="$(qemu-system-x86_64 -display help 2>/dev/null || true)" 103 + for candidate in cocoa gtk sdl curses; do 104 + if echo "$AVAILABLE_DISPLAYS" | /usr/bin/grep -q "^${candidate}\$"; then 105 + DISPLAY_BACKEND="$candidate" 106 + break 107 + fi 108 + done 109 + [ -z "$DISPLAY_BACKEND" ] && DISPLAY_BACKEND="curses" 110 + 111 + echo "Launching QEMU (display=${DISPLAY_BACKEND})..." 112 + echo " The TempleOS installer will come up. Pick a disk/partition" 113 + echo " and say yes to all prompts — when the installer finishes" 114 + echo " and the VM reboots, you land directly in VodBrowser." 115 + echo "" 116 + 117 + QEMU_CMD=( 118 + qemu-system-x86_64 119 + -machine pc-i440fx-10.0,vmport=off,hpet=off 120 + -cpu Skylake-Client 121 + -m 2048 122 + -smp cpus=4,sockets=1,cores=4,threads=1 123 + -accel tcg,tb-size=512 124 + -device cirrus-vga 125 + -display "$DISPLAY_BACKEND" 126 + -device pcnet,netdev=net0 127 + -netdev user,id=net0 128 + # Boot straight from the dist ISO — no hard disk attached, so 129 + # the VM is ephemeral and a re-run always reflects the latest 130 + # rebuild. If you want persistence, build-vodbrowser-dist.sh 131 + # only produces the ISO; use qemu-run.sh for a disk-backed run. 132 + -drive "if=none,id=drive_cd,media=cdrom,file=${OUT_ISO},readonly=on,file.locking=off" 133 + -device ide-cd,bus=ide.1,drive=drive_cd,bootindex=0 134 + -boot order=d 135 + ) 136 + 137 + "${QEMU_CMD[@]}"
+105
deploy.sh
··· 1 + #!/bin/bash 2 + # deploy.sh — Build Net.prg + Media.prg and package a RedSea sources ISO 3 + # that can be mounted in UTM/QEMU and used with TempleOS V5.03. 4 + # 5 + # After deploy, in TempleOS: 6 + # Cd("T:/"); 7 + # #include "VodBrowser.HC" 8 + # VodBrowser; 9 + 10 + set -euo pipefail 11 + 12 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 13 + ISO_DIR="$SCRIPT_DIR/isos" 14 + TOS_ISO="$ISO_DIR/TempleOS.iso" 15 + SOURCES_ISO="$ISO_DIR/sources.iso" 16 + BOOTCODE="$SCRIPT_DIR/tools/bootcode.bin" 17 + 18 + # ---- Bootstrap TempleOS install ISO (needed once for boot-code extraction) ---- 19 + if [ ! -f "$TOS_ISO" ]; then 20 + echo "TempleOS install ISO not found at $TOS_ISO" 21 + read -p "Download TOS_Distro.ISO from templeos.org? [Y/n] " yn 22 + case "$yn" in 23 + [Nn]*) echo "Place it manually at $TOS_ISO"; exit 1 ;; 24 + *) 25 + echo "Downloading..." 26 + curl -L -o "$TOS_ISO" "https://templeos.org/Downloads/TOS_Distro.ISO" 27 + echo "Downloaded to $TOS_ISO" 28 + ;; 29 + esac 30 + fi 31 + 32 + if [ ! -f "$BOOTCODE" ]; then 33 + echo "Extracting bootcode.bin from TempleOS ISO..." 34 + python3 "$SCRIPT_DIR/tools/mkredseaiso.py" \ 35 + --extract-bootcode "$TOS_ISO" \ 36 + --output "$BOOTCODE" 37 + fi 38 + 39 + # ---- Build the two .prg binaries ---- 40 + echo "=== Build Net.prg ===" 41 + make -C "$SCRIPT_DIR/net" elf 42 + 43 + echo "=== Build Media.prg ===" 44 + make -C "$SCRIPT_DIR/media" elf 45 + 46 + # ---- Stamp Version.HC so the ISO carries a build timestamp ---- 47 + BUILD_TS=$(date '+%Y-%m-%d %H:%M:%S') 48 + echo "=== Stamp Version.HC ($BUILD_TS) ===" 49 + cat > "$SCRIPT_DIR/holyc/vodbrowser/lib/Version.HC" <<EOF 50 + // Auto-generated by deploy.sh 51 + U0 Ver() { "VodBrowser build: $BUILD_TS\n"; } 52 + Ver; 53 + EOF 54 + 55 + # ---- Package sources.iso ---- 56 + # The RedSea image is FLAT: mkredseaiso.py saves every file under its 57 + # basename, regardless of how the source tree is organised. So the 58 + # list below can contain files from anywhere in the source tree but 59 + # they must all have unique basenames. Keeping the list explicit 60 + # prevents dead/experimental files from sneaking onto the ISO. 61 + echo "=== Package sources.iso ===" 62 + HC_FILES=( 63 + # External program wrappers 64 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Load.HC" 65 + "$SCRIPT_DIR/holyc/vodbrowser/lib/TosCallbacks.HC" 66 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Nic.HC" 67 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Net.HC" 68 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Media.HC" 69 + "$SCRIPT_DIR/holyc/vodbrowser/lib/Version.HC" 70 + # VodBrowser application 71 + "$SCRIPT_DIR/holyc/vodbrowser/Util.HC" 72 + "$SCRIPT_DIR/holyc/vodbrowser/Prefs.HC" 73 + "$SCRIPT_DIR/holyc/vodbrowser/NetTask.HC" 74 + "$SCRIPT_DIR/holyc/vodbrowser/DecodeTask.HC" 75 + "$SCRIPT_DIR/holyc/vodbrowser/VideoList.HC" 76 + "$SCRIPT_DIR/holyc/vodbrowser/Playback.HC" 77 + "$SCRIPT_DIR/holyc/vodbrowser/Settings.HC" 78 + "$SCRIPT_DIR/holyc/vodbrowser/Menu.HC" 79 + # Top-level entry point (includes all of the above and defines 80 + # BrowseInit / BrowseShutdown / VodBrowser / SoundTest) 81 + "$SCRIPT_DIR/holyc/VodBrowser.HC" 82 + # Install / distro-build helper — user-runnable script that copies 83 + # the VodBrowser tree into C:/Home/ and wires HomeSys.HC to auto- 84 + # launch on boot. Bundled here so a mounted sources.iso is 85 + # everything you need for a full install. 86 + "$SCRIPT_DIR/holyc/MakeVodBrowserISO.HC" 87 + ) 88 + PRG_FILES=( 89 + "$SCRIPT_DIR/net/Net.prg" 90 + "$SCRIPT_DIR/media/Media.prg" 91 + ) 92 + 93 + python3 "$SCRIPT_DIR/tools/mkredseaiso.py" \ 94 + --output "$SOURCES_ISO" \ 95 + --bootcode "$BOOTCODE" \ 96 + "${HC_FILES[@]}" "${PRG_FILES[@]}" 97 + 98 + echo 99 + echo "=== Done ===" 100 + echo "ISO: $SOURCES_ISO" 101 + echo 102 + echo "In TempleOS (after mounting the ISO as a CD):" 103 + echo ' Cd("T:/");' 104 + echo ' #include "VodBrowser.HC"' 105 + echo ' VodBrowser;'
+33
elf/elf2bin.py
··· 1 + #!/usr/bin/env python3 2 + """Extract raw binary from ELF by copying LOAD segments.""" 3 + import struct, sys 4 + 5 + def extract(elf_path, bin_path): 6 + with open(elf_path, 'rb') as f: 7 + elf = f.read() 8 + 9 + e_phoff = struct.unpack_from('<Q', elf, 32)[0] 10 + e_phentsize = struct.unpack_from('<H', elf, 54)[0] 11 + e_phnum = struct.unpack_from('<H', elf, 56)[0] 12 + 13 + result = bytearray() 14 + base_addr = None 15 + for i in range(e_phnum): 16 + off = e_phoff + i * e_phentsize 17 + if struct.unpack_from('<I', elf, off)[0] != 1: # PT_LOAD 18 + continue 19 + p_offset = struct.unpack_from('<Q', elf, off + 8)[0] 20 + p_vaddr = struct.unpack_from('<Q', elf, off + 16)[0] 21 + p_filesz = struct.unpack_from('<Q', elf, off + 32)[0] 22 + if base_addr is None: 23 + base_addr = p_vaddr 24 + local = p_vaddr - base_addr 25 + while len(result) < local + p_filesz: 26 + result.append(0) 27 + result[local:local + p_filesz] = elf[p_offset:p_offset + p_filesz] 28 + 29 + with open(bin_path, 'wb') as f: 30 + f.write(result) 31 + 32 + if __name__ == '__main__': 33 + extract(sys.argv[1], sys.argv[2])
+66
elf/make_program.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + make_program.py — Convert linked ELF binaries into a .prg for TempleOS loader. 4 + 5 + Adapted from Whitham's make_program.py (Python 3). 6 + 7 + Takes unoffset.bin and offset.bin (linked at base 0 and 0x12340000), 8 + compares them to find pointer relocations, and produces a .prg file 9 + with a relocation table appended. 10 + """ 11 + import struct 12 + import sys 13 + 14 + VERSION = 0x1 15 + 16 + def read_qword(data, index): 17 + return struct.unpack_from("<Q", data, index * 8)[0] 18 + 19 + def main(): 20 + with open("unoffset.bin", "rb") as f: 21 + binary_data = f.read() 22 + with open("offset.bin", "rb") as f: 23 + offset_data = f.read() 24 + 25 + assert len(binary_data) == len(offset_data), "Size mismatch" 26 + assert binary_data != offset_data, "Binaries are identical (no relocations detected?)" 27 + 28 + load_size = len(binary_data) 29 + fini_address = read_qword(binary_data, 3) # qword[3] 30 + end_address = read_qword(binary_data, 4) # qword[4] 31 + 32 + assert fini_address <= load_size, f"_fini ({fini_address}) beyond EOF ({load_size})" 33 + assert end_address >= load_size, f"_end ({end_address}) not beyond EOF ({load_size})" 34 + 35 + # Pad to 8-byte alignment 36 + while len(binary_data) % 8: 37 + binary_data += b'\x00' 38 + offset_data += b'\x00' 39 + load_size = len(binary_data) 40 + 41 + # Create output with relocations appended 42 + output = bytearray(binary_data) 43 + reloc_count = 0 44 + 45 + for i in range(fini_address // 8, load_size // 8): 46 + diff = read_qword(offset_data, i) - read_qword(binary_data, i) 47 + if diff != 0: 48 + assert diff == 0x12340000, f"Unexpected diff at qword {i}: 0x{diff:x}" 49 + output.extend(struct.pack("<I", i)) 50 + reloc_count += 1 51 + 52 + total_size = len(output) 53 + 54 + # Patch header 55 + struct.pack_into("<I", output, 8, VERSION) # qword[1] low word = version 56 + struct.pack_into("<Q", output, 5 * 8, load_size) # qword[5] = load_size 57 + struct.pack_into("<Q", output, 6 * 8, total_size) # qword[6] = total_size 58 + 59 + outname = sys.argv[1] if len(sys.argv) > 1 else "program.prg" 60 + with open(outname, "wb") as f: 61 + f.write(output) 62 + 63 + print(f"Created {outname}: {load_size} bytes code + {reloc_count} relocations = {total_size} bytes total") 64 + 65 + if __name__ == "__main__": 66 + main()
+12
elf/offset.x
··· 1 + SECTIONS 2 + { 3 + . = 0x12340000; 4 + .init : { *(.init) } 5 + .fini : { *(.fini) } 6 + .text : { *(.text) *(.text.*) } 7 + .rodata : { *(.rodata) *(.rodata.*) } 8 + .data : { *(.data) *(.data.*) } 9 + .bss : { *(.bss) *(.bss.*) *(COMMON) } 10 + _end = .; 11 + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) *(.debug*) } 12 + }
+59
elf/setup.s
··· 1 + /* 2 + * setup.s — ELF entry stub for Net.prg / Media.prg 3 + * 4 + * Provides the 64-byte .prg header that Load.HC and make_program.py 5 + * both read, plus the _start trampoline that calls into C elf_main. 6 + * 7 + * Header layout (8 qwords, 64 bytes total): 8 + * qword[0] (0x00): jmp to _real_start + "jdw" magic 9 + * qword[1] (0x08): version (patched by make_program.py) 10 + * qword[2] (0x10): reserved (was syscall_address in Whitham's 11 + * Frotz loader; unused here but kept to preserve 12 + * the fixed qword offsets of the slots below) 13 + * qword[3] (0x18): fini_address (set by linker) 14 + * qword[4] (0x20): end_address (set by linker) 15 + * qword[5] (0x28): load_size (patched by make_program.py) 16 + * qword[6] (0x30): total_size (patched by make_program.py) 17 + * qword[7] (0x38): reserved 18 + * 19 + * Entry conventions: loader passes arg1 in RDI and arg2 in RSI, then 20 + * does CALL to _real_start which forwards to elf_main. elf_main's 21 + * return value (in RAX) is the api vtable pointer that HolyC stores. 22 + */ 23 + 24 + .section .init 25 + .org 0x000 26 + .global _start 27 + .global elf_main 28 + .global _end 29 + 30 + _start: 31 + jmp _real_start 32 + .ascii "jdw" /* magic identifier — checked by Load.HC */ 33 + 34 + .org 0x008 35 + version: 36 + .long 0 /* patched by make_program.py */ 37 + .long 0x1 38 + reserved_syscall: 39 + .quad 0 /* unused slot, preserves header layout */ 40 + fini_address: 41 + .quad fini_end /* filled by linker */ 42 + end_address: 43 + .quad _end /* filled by linker */ 44 + load_size: 45 + .quad 0 /* patched by make_program.py */ 46 + total_size: 47 + .quad 0 /* patched by make_program.py */ 48 + reserved: 49 + .quad 0 50 + 51 + .section .fini 52 + .align 4 53 + fini_end: 54 + 55 + .text 56 + _real_start: 57 + /* RDI and RSI already hold arg1/arg2 from the loader's CALL. */ 58 + call elf_main 59 + retq
+12
elf/unoffset.x
··· 1 + SECTIONS 2 + { 3 + . = 0x0; 4 + .init : { *(.init) } 5 + .fini : { *(.fini) } 6 + .text : { *(.text) *(.text.*) } 7 + .rodata : { *(.rodata) *(.rodata.*) } 8 + .data : { *(.data) *(.data.*) } 9 + .bss : { *(.bss) *(.bss.*) *(COMMON) } 10 + _end = .; 11 + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) *(.debug*) } 12 + }
+189
holyc/MakeVodBrowserISO.HC
··· 1 + // MakeVodBrowserISO.HC — install VodBrowser into a TempleOS system 2 + // and (optionally) drive the distro ISO builder so the resulting 3 + // image auto-launches VodBrowser on boot. 4 + // 5 + // Usage from the TempleOS REPL, with sources.iso mounted as T: — 6 + // the standard "Cd(\"T:/\"); #include this file;" pattern that 7 + // every other VodBrowser script uses: 8 + // 9 + // Cd("T:/"); 10 + // #include "MakeVodBrowserISO.HC"; 11 + // MakeVodBrowserISO; 12 + // 13 + // What it does, in order: 14 + // 1. Creates C:/Home/VodBrowser/ and copies every VodBrowser 15 + // source file (.HC) and binary (.prg) from T: into it. Files 16 + // are listed explicitly because the sources ISO is FLAT — 17 + // every file lives at T:/basename, regardless of its original 18 + // location in the source tree — so we need to know the list 19 + // up front. Adding a new file to the deploy means adding it 20 + // here too. 21 + // 2. Appends a small launcher block to C:/Home/HomeSys.HC so the 22 + // next TempleOS login auto-runs `VodBrowser;`. The block is 23 + // fenced with a marker comment; running this script twice is 24 + // a no-op (re-runs detect the marker and skip the append). 25 + // 3. Invokes the TempleOS distro build so the new HD state gets 26 + // packaged as a bootable ISO. See BuildDistroISO below — the 27 + // exact entry point has varied slightly between TempleOS 28 + // builds, so the call site is commented with the fallbacks 29 + // we've seen in the wild. 30 + // 31 + // After the script completes, reboot from the generated ISO (or 32 + // simply reboot the current TempleOS install — HomeSys.HC will 33 + // launch VodBrowser either way). 34 + 35 + #define VOD_INSTALL_DIR "C:/Home/VodBrowser" 36 + #define VOD_HOMESYS "C:/Home/HomeSys.HC" 37 + #define VOD_MARKER "// --- VodBrowser autolaunch (added by MakeVodBrowserISO) ---" 38 + 39 + // Canonical list of every file that ships on the sources ISO. Kept 40 + // in lockstep with deploy.sh's HC_FILES / PRG_FILES arrays — if you 41 + // add a new .HC or .prg to the deploy you also need to add it here. 42 + U8 *vod_files[] = { 43 + // External program wrappers 44 + "Load.HC", 45 + "TosCallbacks.HC", 46 + "Nic.HC", 47 + "Net.HC", 48 + "Media.HC", 49 + "Version.HC", 50 + // VodBrowser subsystems 51 + "Util.HC", 52 + "Prefs.HC", 53 + "NetTask.HC", 54 + "DecodeTask.HC", 55 + "VideoList.HC", 56 + "Playback.HC", 57 + "Settings.HC", 58 + "Menu.HC", 59 + // Top-level entry point 60 + "VodBrowser.HC", 61 + // Pre-built C libraries 62 + "Net.prg", 63 + "Media.prg", 64 + }; 65 + 66 + U0 CopyVodBrowserFiles() 67 + { 68 + "Copying VodBrowser files to %s/ ...\n", VOD_INSTALL_DIR; 69 + 70 + // DirMk is idempotent — silent no-op if the directory already 71 + // exists. We create the parent C:/Home first in case this is a 72 + // fresh install where Home doesn't exist yet. 73 + DirMk("C:/Home"); 74 + DirMk(VOD_INSTALL_DIR); 75 + 76 + I64 n = sizeof(vod_files) / sizeof(vod_files[0]); 77 + I64 i; 78 + I64 copied = 0; 79 + for (i = 0; i < n; i++) { 80 + U8 src[192], dst[192]; 81 + StrPrint(src, "T:/%s", vod_files[i]); 82 + StrPrint(dst, "%s/%s", VOD_INSTALL_DIR, vod_files[i]); 83 + if (!FileFind(src)) { 84 + " [missing on source ISO] %s\n", vod_files[i]; 85 + continue; 86 + } 87 + Copy(src, dst); 88 + copied++; 89 + } 90 + "Copied %d / %d files.\n", copied, n; 91 + } 92 + 93 + U0 WriteHomeSysLauncher() 94 + { 95 + // Build the launcher block. Auto-Cd into the install dir so 96 + // VodBrowser.HC's bare-basename #includes resolve, include the 97 + // top-level script, and run the entry point. VodBrowser returns 98 + // cleanly on ESC so the user still has the shell if they want it. 99 + U8 launcher[1024]; 100 + StrPrint(launcher, 101 + "\n%s\n" 102 + "Cd(\"%s\");\n" 103 + "#include \"VodBrowser.HC\"\n" 104 + "VodBrowser;\n", 105 + VOD_MARKER, VOD_INSTALL_DIR); 106 + 107 + // If HomeSys.HC already contains our marker, don't append again — 108 + // running this script twice should be a no-op rather than 109 + // accumulating duplicate launcher blocks. 110 + if (FileFind(VOD_HOMESYS)) { 111 + I64 sz = 0; 112 + U8 *existing = FileRead(VOD_HOMESYS, &sz); 113 + Bool already = FALSE; 114 + if (existing) { 115 + // StrFind returns NULL if the substring isn't in the buffer. 116 + if (StrFind(VOD_MARKER, existing)) already = TRUE; 117 + Free(existing); 118 + } 119 + if (already) { 120 + "HomeSys.HC already has VodBrowser launcher — skipping.\n"; 121 + return; 122 + } 123 + 124 + // Append to the existing HomeSys.HC by reading, concatenating, 125 + // writing back. TempleOS FileWrite overwrites rather than 126 + // appending, so we do the concat by hand. 127 + U8 *existing2 = FileRead(VOD_HOMESYS, &sz); 128 + if (!existing2) { 129 + "HomeSys.HC read failed — aborting HomeSys update.\n"; 130 + return; 131 + } 132 + I64 ll = StrLen(launcher); 133 + U8 *combined = MAlloc(sz + ll + 1); 134 + MemCpy(combined, existing2, sz); 135 + MemCpy(combined + sz, launcher, ll); 136 + combined[sz + ll] = 0; 137 + FileWrite(VOD_HOMESYS, combined, sz + ll); 138 + Free(existing2); 139 + Free(combined); 140 + "HomeSys.HC updated (appended launcher).\n"; 141 + } else { 142 + // Fresh file — write just the launcher. 143 + FileWrite(VOD_HOMESYS, launcher, StrLen(launcher)); 144 + "HomeSys.HC written (new file).\n"; 145 + } 146 + } 147 + 148 + U0 BuildDistroISO() 149 + { 150 + // TempleOS V5.03 ships a distro-builder, but the exact entry 151 + // point isn't universal — different community builds have 152 + // renamed it over the years. Most commonly one of: 153 + // 154 + // Distro; // Original Terry convention 155 + // BootHDIns; // Re-installs to the HD (not an ISO) 156 + // MakeAdamISO; // Some rebuild forks expose this 157 + // 158 + // Rather than guess, this script prints the common incantations 159 + // and leaves the actual build step to the user — a wrong guess 160 + // here would be worse than a clear prompt, because the distro 161 + // build can clobber the running system if it picks the wrong 162 + // path. Once you confirm the name on your TempleOS install, 163 + // uncomment the matching line below and re-run. 164 + "\n"; 165 + "To package the new HD state as a bootable ISO, run ONE of:\n"; 166 + " Distro; # original TempleOS V5.03 distro builder\n"; 167 + " BootHDIns; # re-installs boot code to the current HD\n"; 168 + "\n"; 169 + "(the script didn't call it automatically because the exact\n"; 170 + " entry point differs between TempleOS builds and a wrong call\n"; 171 + " can clobber the running system — confirm the name on your\n"; 172 + " install first)\n"; 173 + } 174 + 175 + U0 MakeVodBrowserISO() 176 + { 177 + "==== VodBrowser -> TempleOS install ====\n\n"; 178 + CopyVodBrowserFiles; 179 + "\n"; 180 + WriteHomeSysLauncher; 181 + BuildDistroISO; 182 + "\n==== Done ====\n"; 183 + "\n"; 184 + "Next steps:\n"; 185 + " 1. (optional) run the distro-build command printed above\n"; 186 + " 2. Reboot — HomeSys.HC will auto-launch VodBrowser\n"; 187 + "\n"; 188 + "Press Esc inside VodBrowser to drop back to the REPL.\n"; 189 + }
+364
holyc/VodBrowser.HC
··· 1 + // VodBrowser.HC — ATProto video browser for TempleOS 2 + // 3 + // Usage from the TempleOS REPL: 4 + // Cd("T:/"); 5 + // #include "VodBrowser.HC"; 6 + // VodBrowser; 7 + // 8 + // Architecture (3 tasks): 9 + // Core 0 Main task — UI, menu, blocks on PlayVideoStream 10 + // Core 1 NetTask — owns Net.prg, runs HLS streaming pipeline 11 + // Core 1/2 DecodeTask — owns Media.prg, H.264 decode + dither 12 + // 13 + // Each worker task is Spawned with a generous stk_size and runs its 14 + // C code directly on that stack. Do NOT reintroduce the heap big-stack 15 + // trick — TempleOS's WallPaper task runs UnusedStk(task) and panics 16 + // "Stk Overflow" when rsp is outside the task's stack range, which 17 + // happens under preemption if rsp points into a heap buffer. 18 + // 19 + // Video rendering uses Fs->draw_it + SettingsPush/Pop so WinMgr does 20 + // the compositing and automatic cleanup. Nothing writes to gr.dc 21 + // directly. 22 + // 23 + // Implementation lives in vodbrowser/*.HC and vodbrowser/lib/*.HC in 24 + // the source tree, but the RedSea ISO we ship is FLAT — every file 25 + // ends up in the root of T:/ as its basename (see tools/mkredseaiso.py: 26 + // `name = basename(path)`). So the #include directives below use bare 27 + // filenames even though the on-disk source is organised into subfolders. 28 + // 29 + // Includes MUST be in strict dependency order because HolyC is a 30 + // single-pass compiler: every symbol has to be declared before its 31 + // first use. 32 + 33 + // External program wrappers (vodbrowser/lib/*.HC in the source tree) 34 + #include "Net.HC" 35 + #include "Media.HC" 36 + 37 + // VodBrowser subsystems (vodbrowser/*.HC in the source tree) 38 + #include "Util.HC" 39 + #include "Prefs.HC" 40 + #include "NetTask.HC" 41 + #include "DecodeTask.HC" 42 + #include "VideoList.HC" 43 + #include "Playback.HC" 44 + #include "Settings.HC" 45 + #include "Menu.HC" 46 + 47 + // ================================================================== 48 + // Top-level task handles, init / shutdown, entry point 49 + // ================================================================== 50 + 51 + CTask *net_task; 52 + CTask *decode_task; 53 + 54 + // ================================================================== 55 + // Init / Shutdown 56 + // ================================================================== 57 + 58 + I64 BrowseInit() 59 + { 60 + // Capture the main task's absolute cur_dir + drive letter into 61 + // g_vod_dir so NetInit / MediaInit can build drive-qualified 62 + // paths for ElfLoad. TempleOS's Spawn resets every child task's 63 + // cur_dir to /Home (Kernel/KTask.HC), so the worker tasks can't 64 + // just read Fs->cur_dir themselves — we have to snapshot it 65 + // here, in the parent, before spawning. 66 + // 67 + // cur_dir is a bare path like "/VodBrowser" or "/" without the 68 + // drive letter (CDrv tracks that separately as cur_dv->drv_let), 69 + // so we prepend "X:" manually. Normalise to ALWAYS end with a 70 + // trailing slash — otherwise the root-of-drive case produces 71 + // "T:/" which yields "T://Net.prg" when you format with "/" 72 + // separator in front of the filename. With a guaranteed trailing 73 + // slash, consumers can just use "%sNet.prg" and the path is 74 + // correct for both "T:/" and "T:/VodBrowser". 75 + // 76 + // Coverage: 77 + // - sources.iso dev: user Cd'd to "T:/" → "T:/" 78 + // - distro live CD: HomeSys.HC Cd'd to "::/VodBrowser" → "T:/VodBrowser/" 79 + // - installed HD: wherever the user Cd'd → e.g. "C:/Home/VodBrowser/" 80 + StrPrint(g_vod_dir, "%C:%s", Fs->cur_dv->drv_let, Fs->cur_dir); 81 + I64 vdl = StrLen(g_vod_dir); 82 + if (vdl > 0 && g_vod_dir[vdl - 1] != '/') { 83 + g_vod_dir[vdl] = '/'; 84 + g_vod_dir[vdl + 1] = 0; 85 + } 86 + 87 + // Load prefs first — NetTask reads pds/did/vod_base/src_quality 88 + // immediately on startup. If the prefs file doesn't exist yet we 89 + // DON'T auto-save a fresh one on boot: on a live CD the drive is 90 + // read-only and TempleOS's FileWrite → RedSeaFileWrite → BlkWrite 91 + // chain hard-panics with a BlkDev exception that bypasses HolyC's 92 + // try/catch. The in-memory defaults from PrefsDefaults are 93 + // perfectly usable without the file; first persistence happens 94 + // the next time the user hits Save in the Settings menu, where a 95 + // read-only-medium failure is a reasonable outcome instead of a 96 + // crash on first boot. 97 + PrefsDefaults(&g_prefs); 98 + if (PrefsLoad(&g_prefs, PREFS_PATH) < 0) { 99 + "Prefs: no %s, using defaults\n", PREFS_PATH; 100 + } else { 101 + "Prefs: loaded from %s\n", PREFS_PATH; 102 + } 103 + PrefsApply(&g_prefs); 104 + 105 + // Pre-allocate the ring buffer slots once. They're reused for the 106 + // entire Browse session — no per-segment MAlloc/Free churn. 107 + I64 ri; 108 + for (ri = 0; ri < HLS_RING_SIZE; ri++) { 109 + hls_ring[ri] = MAlloc(HLS_RING_SLOT_SIZE); 110 + hls_ring_size[ri] = 0; 111 + } 112 + hls_ring_head = 0; 113 + hls_ring_tail = 0; 114 + hls_ring_count = 0; 115 + 116 + // Reset stale state from any prior run before spawning. 117 + net_ready = FALSE; 118 + net_ip = 0; 119 + net_dbg_ready = FALSE; 120 + dec_dbg_ready = FALSE; 121 + 122 + I64 ncpus = mp_cnt; 123 + "CPUs: %d, multicore=%d\n", ncpus, g_prefs.multicore; 124 + 125 + // Build the core assignment policy from the multicore pref. 126 + CorePolicyBuild(&g_core_policy, g_prefs.multicore, ncpus); 127 + "Cores: net=%d dec=%d\n", g_core_policy.net_core, g_core_policy.dec_core; 128 + 129 + // Worker tasks get large task stacks so their C code (lwIP+BearSSL 130 + // for net, openh264 for media) runs on the real task stack, NOT on 131 + // a heap buffer via call_on_big_stack. See comment at top of this 132 + // application. 133 + net_run = TRUE; 134 + net_msg = NET_MSG_NONE; 135 + net_resp_done = FALSE; 136 + net_task = Spawn(&NetTaskFn, NULL, "NetTask", 137 + g_core_policy.net_core, Fs, 4*1024*1024); 138 + 139 + dec_init_ok = FALSE; 140 + dec_run = TRUE; 141 + decode_task = Spawn(&DecodeTaskFn, NULL, "Decode", 142 + g_core_policy.dec_core, Fs, 8*1024*1024); 143 + 144 + // Wait for DecodeTask to finish MediaInit (loads Media.prg, builds 145 + // the thunk, populates media_api_table). Only after this is it safe 146 + // to call any Media.prg thunk from the main task. Timeout at ~5s 147 + // so a broken Media.prg doesn't hang Browse forever. 148 + I64 t0 = cnts.jiffies; 149 + while (!dec_init_ok && (cnts.jiffies - t0) < JIFFY_FREQ * 5) { 150 + DrainDebug; 151 + Sleep(20); 152 + } 153 + if (dec_init_ok) { 154 + // Push the initial dither mode and skip-non-ref setting into 155 + // Media.prg. DecodeTask is idle in its outer wait loop 156 + // (hls_streaming is FALSE), so the thunk globals are uncontested. 157 + MediaSetDitherMode(g_prefs.dither_mode); 158 + MediaSetSkipNonRef(g_prefs.skip_non_ref); 159 + } else { 160 + "Warning: DecodeTask init timed out\n"; 161 + } 162 + 163 + return 0; 164 + } 165 + 166 + U0 BrowseShutdown() 167 + { 168 + dec_run = FALSE; 169 + Sleep(100); 170 + if (decode_task) { Kill(decode_task); decode_task = NULL; } 171 + 172 + net_run = FALSE; 173 + Sleep(100); 174 + if (net_task) { Kill(net_task); net_task = NULL; } 175 + 176 + // Release the pre-allocated ring slots. Both worker tasks are 177 + // gone by the time we get here, so there's nothing left that 178 + // could be holding a slot pointer. 179 + I64 ri; 180 + for (ri = 0; ri < HLS_RING_SIZE; ri++) { 181 + if (hls_ring[ri]) { Free(hls_ring[ri]); hls_ring[ri] = NULL; } 182 + hls_ring_size[ri] = 0; 183 + } 184 + } 185 + 186 + // ================================================================== 187 + // Main task → worker bridging 188 + // ================================================================== 189 + 190 + U32 BWaitDhcp() 191 + { 192 + "Waiting for network"; 193 + I64 t0 = cnts.jiffies; 194 + while (!net_ready) { 195 + DrainDebug; 196 + "."; 197 + Sleep(200); 198 + if (cnts.jiffies - t0 > JIFFY_FREQ * 30) { 199 + "\nBWaitDhcp: timeout\n"; 200 + break; 201 + } 202 + } 203 + DrainDebug; 204 + "\n"; 205 + if (net_ip) 206 + "IP: %d.%d.%d.%d\n", 207 + net_ip & 0xFF, (net_ip >> 8) & 0xFF, 208 + (net_ip >> 16) & 0xFF, (net_ip >> 24) & 0xFF; 209 + return net_ip; 210 + } 211 + 212 + // ================================================================== 213 + // Main entry 214 + // ================================================================== 215 + 216 + U0 VodBrowser() 217 + { 218 + if (BrowseInit < 0) return; 219 + 220 + U32 ip = BWaitDhcp; 221 + if (!ip) { "No IP\n"; BrowseShutdown; return; } 222 + 223 + DocClear; 224 + if (FetchVideos < 0) { BrowseShutdown; return; } 225 + 226 + // Fullscreen the window for the duration of VodBrowser. SettingsPop 227 + // at the end restores the caller's original border / max state, 228 + // so when VodBrowser returns the REPL window looks as it did before. 229 + // PlayVideoStream nests its own SettingsPush/Pop, which cleanly 230 + // stacks on top of ours without disturbing it. 231 + SettingsPush; 232 + WinBorder; // toggle border off for a clean edge 233 + WinMax; // full-screen 234 + DocCursor; // hide the text cursor (display-only UI) 235 + 236 + menu_page_idx = 0; 237 + menu_sel_idx = 0; 238 + 239 + I64 ch, sc; 240 + Bool done = FALSE; 241 + while (!done) { 242 + MenuClampSel; 243 + RenderMenuPage; 244 + 245 + // Wait for a key or a mouse click. Click support lets the user 246 + // tap a tile / footer button directly instead of arrow-navigating 247 + // to it and pressing Enter; internally we set menu_sel_idx to the 248 + // hit target and then fall through to the normal Enter handler 249 + // by synthesising ch='\n'. DolDoc's native marquee on long tile 250 + // titles animates itself, so we don't need a render timer in 251 + // this wait loop. 252 + Bool click_activate = FALSE; 253 + while (TRUE) { 254 + if (ScanKey(&ch, &sc)) break; 255 + Bool click = MouseClickEdge; 256 + if (click) { 257 + I64 hit = HitFind(MouseTextCol, MouseTextRow); 258 + if (hit >= 0) { 259 + menu_sel_idx = hit; 260 + click_activate = TRUE; 261 + break; 262 + } 263 + } 264 + Sleep(20); 265 + } 266 + FlushMsgs; 267 + 268 + if (click_activate) { 269 + ch = '\n'; 270 + sc = 0; 271 + } 272 + 273 + I64 k = sc & 0xFF; 274 + 275 + // Hotkeys that fire regardless of current selection. 276 + if (ch == CH_ESC) { done = TRUE; } 277 + else { 278 + if (ch == 's' || ch == 'S') ShowSettings; 279 + 280 + if (ch == '\n') { 281 + // Enter activates whatever is currently highlighted. 282 + if (menu_sel_idx == MENU_SEL_SETTINGS) { 283 + ShowSettings; 284 + } else if (menu_sel_idx == MENU_SEL_EXIT) { 285 + done = TRUE; 286 + } else if (menu_sel_idx == MENU_SEL_PREV) { 287 + if (menu_page_idx > 0) menu_page_idx--; 288 + } else if (menu_sel_idx == MENU_SEL_NEXT) { 289 + I64 np = (vid_count + PAGE_SIZE - 1) / PAGE_SIZE; 290 + if (np < 1) np = 1; 291 + if (menu_page_idx + 1 < np) { 292 + menu_page_idx++; 293 + } else if (!vid_exhausted && vid_count < MAX_VIDEOS_CAP) { 294 + // Fetch the next batch on demand, then flip forward. 295 + "\nLoading more...\n"; 296 + I64 added = FetchVideoBatch; 297 + if (added > 0) menu_page_idx++; 298 + } 299 + } else { 300 + I64 play_idx = menu_page_idx * PAGE_SIZE + menu_sel_idx; 301 + if (play_idx < vid_count) 302 + PlayVideoStream(vid_uris[play_idx], vid_titles[play_idx]); 303 + } 304 + } 305 + 306 + if (ch == 0) { 307 + // Grid-to-footer and within-footer navigation. Grid slots 308 + // 0..5 form a 2x3; footer slots 6..9 are Prev / Next / 309 + // Settings / Exit in left-to-right order. 310 + if (k == SC_CURSOR_UP) { 311 + if (menu_sel_idx == MENU_SEL_PREV) menu_sel_idx = 3; 312 + else if (menu_sel_idx == MENU_SEL_NEXT) menu_sel_idx = 4; 313 + else if (menu_sel_idx == MENU_SEL_SETTINGS) menu_sel_idx = 5; 314 + else if (menu_sel_idx == MENU_SEL_EXIT) menu_sel_idx = 5; 315 + else if (menu_sel_idx >= 3) menu_sel_idx -= 3; 316 + } 317 + if (k == SC_CURSOR_DOWN) { 318 + if (menu_sel_idx >= 3 && menu_sel_idx < PAGE_SIZE) { 319 + // Bottom row → Next button (most common action by far). 320 + // Left/Right in the footer reaches Prev / Prefs / Back. 321 + menu_sel_idx = MENU_SEL_NEXT; 322 + } else if (menu_sel_idx < 3) { 323 + menu_sel_idx += 3; 324 + } 325 + } 326 + if (k == SC_CURSOR_LEFT) { 327 + if (menu_sel_idx == MENU_SEL_EXIT) menu_sel_idx = MENU_SEL_SETTINGS; 328 + else if (menu_sel_idx == MENU_SEL_SETTINGS) menu_sel_idx = MENU_SEL_NEXT; 329 + else if (menu_sel_idx == MENU_SEL_NEXT) menu_sel_idx = MENU_SEL_PREV; 330 + else if (menu_sel_idx == MENU_SEL_PREV) { /* stay */ } 331 + else if (menu_sel_idx > 0) menu_sel_idx--; 332 + } 333 + if (k == SC_CURSOR_RIGHT) { 334 + if (menu_sel_idx == MENU_SEL_PREV) menu_sel_idx = MENU_SEL_NEXT; 335 + else if (menu_sel_idx == MENU_SEL_NEXT) menu_sel_idx = MENU_SEL_SETTINGS; 336 + else if (menu_sel_idx == MENU_SEL_SETTINGS) menu_sel_idx = MENU_SEL_EXIT; 337 + else if (menu_sel_idx == MENU_SEL_EXIT) { /* stay */ } 338 + else if (menu_sel_idx + 1 < PAGE_SIZE) menu_sel_idx++; 339 + } 340 + } 341 + } 342 + } 343 + 344 + SettingsPop; // restore pre-VodBrowser window state 345 + 346 + // Frees every loaded title/uri and resets vid_count + cursor state 347 + // so a second `VodBrowser;` invocation starts clean. Inlining the 348 + // free loop here used to leave the pointers dangling — a subsequent 349 + // FetchVideos() would call ResetVideoList again and double-free. 350 + ResetVideoList; 351 + 352 + BrowseShutdown; 353 + } 354 + 355 + // Parse-time launch hint. Top-level statements in HolyC run 356 + // when the file is #include'd, so this fires automatically after 357 + // every successful include — once per login on the distro ISO 358 + // (via HomeSys.HC's XTalk'd #include, in both user tasks) and 359 + // once per session on the sources.iso dev workflow. Keeping the 360 + // hint here instead of XTalk'ing it in HomeSys avoids the 361 + // input-queue race where the Tour doc was eating keystrokes out 362 + // of our XTalk'd string literal. 363 + "\n$$FG,BLUE$$VodBrowser loaded. " 364 + "Type $$FG,LTRED$$VodBrowser;$$FG,BLUE$$ to launch.$$FG$$\n\n";
+248
holyc/vodbrowser/DecodeTask.HC
··· 1 + // vodbrowser/DecodeTask.HC — the background video decoder task 2 + // 3 + // Runs on its own core (1 or 2, depending on multicore policy). Owns 4 + // Media.prg — openh264 + libmov — because the Media thunk globals 5 + // (g_cfn, g_a1..g_a4) are not reentrant. Only this task may call 6 + // Media* functions; the main task reads results via the dec_* globals 7 + // which are plain I64 loads. 8 + // 9 + // Session lifecycle: sit idle until NetTask flips hls_streaming = TRUE, 10 + // then drain the ring via DecodeDrainSession. Video only — real audio 11 + // output isn't supported on TempleOS V5.03 (QEMU's PC speaker and 12 + // direct DAC paths are both non-viable, and ISA DMA requires sub-16 13 + // MiB memory that TempleOS can't allocate). 14 + // 15 + // Depends on: vodbrowser/lib/Media.HC (MediaOpen etc.), 16 + // vodbrowser/Prefs.HC (g_prefs reads), vodbrowser/NetTask.HC 17 + // (hls_ring, hls_streaming, pv_stop_requested). 18 + 19 + I64 dec_run; 20 + U64 dec_ctx; 21 + I64 dec_vw, dec_vh; 22 + U8 *dec_frame_pal; 23 + I64 dec_frame_w, dec_frame_h; 24 + I64 dec_frame_seq; 25 + I64 dec_eof; 26 + 27 + // Count of segments that failed to open during the current playback. 28 + // Surfaced in the post-playback summary line regardless of debug mode 29 + // so the user can tell when the stream is unhealthy. 30 + I64 dec_open_fails; 31 + 32 + // ---- Frame pacing state ---- 33 + // 34 + // Sleep side: HolyC tracks first_pts_ms and segment_start_ms per 35 + // segment so we can Sleep() when the decoder gets ahead of source 36 + // wallclock. Skip side: the C side in Media.prg's on_frame does the 37 + // skip decision dynamically, comparing PTS vs sys_now(). See 38 + // media_api.c for the C-side logic. 39 + // 40 + // dec_sess_display_count is the number of frames that made it to 41 + // the screen. dec_sess_skip_count is read from Media.prg at the 42 + // end of each segment (MediaGetSkipCount) and accumulated. 43 + #define DEC_SLEEP_THRESHOLD_MS 4 // below this, don't bother sleeping 44 + #define DEC_SLEEP_MAX_MS 30 // cap so ESC stays responsive 45 + 46 + I64 dec_sess_display_count; 47 + I64 dec_sess_skip_count; 48 + 49 + // Set TRUE by DecodeTask after MediaInit succeeds. The main task 50 + // polls this after Spawn to know when Media.prg has finished loading. 51 + I64 dec_init_ok; 52 + 53 + // ================================================================== 54 + // DecodeTask — runs on core 1/2, owns Media.prg 55 + // ================================================================== 56 + 57 + // Compute the target scale (sw,sh) for a source video of (ww,hh) 58 + // pixels, honouring settings_disp_w/h (0 = Native). Aspect ratio 59 + // preserved. Pulled out of DecodeTaskFn so the main task function 60 + // stays small enough for HolyC to compile cleanly. 61 + U0 DecodePickScale(I64 ww, I64 hh, I64 *out_sw, I64 *out_sh) 62 + { 63 + I64 sw = ww, sh = hh; 64 + I64 tw = g_prefs.disp_w; 65 + I64 th = g_prefs.disp_h; 66 + if (tw > 0 && th > 0 && ww > 0 && hh > 0 && (ww > tw || hh > th)) { 67 + sw = tw; 68 + sh = th; 69 + if (ww * sh > hh * sw) sh = hh * sw / ww; 70 + else sw = ww * sh / hh; 71 + } 72 + *out_sw = sw; 73 + *out_sh = sh; 74 + } 75 + 76 + // Process one HLS segment end-to-end: open, play every frame to EOF, 77 + // close. On bad input or open failure we bump dec_open_fails and 78 + // return. Called by DecodeTaskFn's drain loop — extracted to keep 79 + // that loop's compiled body small. 80 + U0 DecodeOneSegment(U8 *frag, I64 fsz) 81 + { 82 + // Guard against garbage ring slots. If the pointer looks bogus or 83 + // the size is out of range, log and skip rather than crash inside 84 + // MediaOpen / libmov. 85 + if (!frag || fsz <= 0 || fsz > 16 * 1024 * 1024 || frag(U64) < 0x10000) { 86 + StrPrint(dec_dbg, "bad frag: 0x%X size=%d", frag, fsz); 87 + dec_dbg_ready = TRUE; 88 + dec_open_fails++; 89 + return; 90 + } 91 + 92 + U64 ctx = MediaOpen(frag, fsz); 93 + if (!ctx) { 94 + dec_open_fails++; 95 + StrPrint(dec_dbg, "open fail seg (%d total)", dec_open_fails); 96 + dec_dbg_ready = TRUE; 97 + return; 98 + } 99 + 100 + I64 ww = 0, hh = 0; 101 + MediaVideoInfo(ctx, &ww, &hh); 102 + 103 + I64 sw = 0, sh = 0; 104 + DecodePickScale(ww, hh, &sw, &sh); 105 + 106 + if (!dec_frame_pal || dec_vw != sw || dec_vh != sh) { 107 + if (dec_frame_pal) Free(dec_frame_pal); 108 + dec_frame_pal = MAlloc(sw * sh); 109 + } 110 + dec_vw = sw; dec_vh = sh; 111 + 112 + // First successful open unblocks PlayVideoStream's pre-render wait. 113 + hls_first_ready = TRUE; 114 + 115 + // Expose ctx so debugging tools can inspect the current decoder. 116 + dec_ctx = ctx; 117 + 118 + // Per-segment pacing clock. first_pts_ms stays -1 until the first 119 + // decoded frame anchors it; segment_start_ms tracks wallclock at 120 + // that same moment. Both are used purely for the "sleep when 121 + // ahead" side of pacing — the "skip when behind" decision lives 122 + // inside Media.prg's on_frame, which reads sys_now() and the same 123 + // PTS baseline independently. See media_api.c for that logic. 124 + // 125 + // MediaGetSkipCount is cumulative across the ctx's lifetime, so 126 + // we snapshot it at segment start and take the delta at segment 127 + // end to get this segment's skip contribution. 128 + I64 first_pts_ms = -1; 129 + I64 segment_start_ms = 0; 130 + I64 skip_count_before = MediaGetSkipCount(ctx); 131 + 132 + dec_eof = FALSE; 133 + while (dec_run && !dec_eof && !pv_stop_requested) { 134 + I64 ft = MediaNext(ctx); 135 + if (ft != MEDIA_VIDEO) { 136 + // MEDIA_EOF or MEDIA_ERROR — segment drained. 137 + dec_eof = TRUE; 138 + } else { 139 + I64 pts_ms = MediaGetPts(ctx); 140 + I64 now_ms = cnts.jiffies * 1000 / JIFFY_FREQ; 141 + 142 + if (first_pts_ms < 0 && pts_ms >= 0) { 143 + first_pts_ms = pts_ms; 144 + segment_start_ms = now_ms; 145 + } 146 + 147 + // Sleep when we're ahead of wallclock. We never skip frames 148 + // at this level anymore — if the decoder had to skip a 149 + // non-reference frame to keep up, it already did so inside 150 + // on_frame and MediaNext returned the next reference frame. 151 + if (first_pts_ms >= 0) { 152 + I64 target_ms = pts_ms - first_pts_ms; 153 + I64 elapsed_ms = now_ms - segment_start_ms; 154 + I64 lead_ms = target_ms - elapsed_ms; 155 + if (lead_ms > DEC_SLEEP_THRESHOLD_MS) { 156 + if (lead_ms > DEC_SLEEP_MAX_MS) lead_ms = DEC_SLEEP_MAX_MS; 157 + Sleep(lead_ms); 158 + } 159 + } 160 + 161 + I64 fw = dec_vw, fh = dec_vh; 162 + MediaGetPal(ctx, dec_frame_pal, &fw, &fh); 163 + dec_frame_w = fw; 164 + dec_frame_h = fh; 165 + dec_frame_seq++; 166 + dec_sess_display_count++; 167 + } 168 + } 169 + 170 + // Accumulate this segment's C-side skip delta into the session 171 + // counter so the post-playback summary reports it correctly. 172 + I64 skip_count_after = MediaGetSkipCount(ctx); 173 + dec_sess_skip_count += skip_count_after - skip_count_before; 174 + 175 + MediaClose(ctx); 176 + dec_ctx = 0; 177 + } 178 + 179 + // Drain the ring for one playback session. Exits when producer_done 180 + // and the ring is empty, when dec_run flips off, or when the user 181 + // pressed ESC (pv_stop_requested). 182 + // 183 + // Ring slots are pre-allocated in BrowseInit and reused for the whole 184 + // Browse session — we only zero the size field (to mark the slot 185 + // reusable) and advance the tail. No Free() per segment. 186 + U0 DecodeDrainSession() 187 + { 188 + I64 drain_done = FALSE; 189 + while (dec_run && !drain_done) { 190 + if (pv_stop_requested) { drain_done = TRUE; } 191 + else if (hls_ring_count <= 0) { 192 + if (hls_producer_done) drain_done = TRUE; 193 + else Sleep(5); 194 + } else { 195 + U8 *frag = hls_ring[hls_ring_tail]; 196 + I64 fsz = hls_ring_size[hls_ring_tail]; 197 + DecodeOneSegment(frag, fsz); 198 + 199 + hls_ring_size[hls_ring_tail] = 0; 200 + hls_ring_tail = (hls_ring_tail + 1) % HLS_RING_SIZE; 201 + hls_ring_count--; 202 + } 203 + } 204 + } 205 + 206 + U0 DecodeTaskFn(I64) 207 + { 208 + // Suppress init banners from ElfLoad / NicInit / MediaInit — they 209 + // otherwise land on the menu's DolDoc and overlay the tile grid. 210 + // elf_quiet is a shared global, so NetTaskFn also sets it; we set 211 + // it here too in case this task wins the spawn race. 212 + elf_quiet = TRUE; 213 + 214 + StrPrint(dec_dbg, "DecodeTask: before MediaInit"); 215 + dec_dbg_ready = TRUE; 216 + 217 + if (MediaInit < 0) { 218 + StrPrint(dec_dbg, "DecodeTask: init failed"); dec_dbg_ready = TRUE; 219 + dec_run = FALSE; 220 + return; 221 + } 222 + StrPrint(dec_dbg, "DecodeTask: ready"); dec_dbg_ready = TRUE; 223 + 224 + // Signal the main task that Media.prg is loaded and the thunk is 225 + // ready. The main task pushes the initial audio pref from there. 226 + dec_init_ok = TRUE; 227 + 228 + dec_ctx = 0; 229 + dec_frame_pal = NULL; 230 + dec_frame_seq = 0; 231 + dec_eof = FALSE; 232 + 233 + // Outer loop: one iteration per playback session. We sit idle until 234 + // NetTask sets hls_streaming, then drain the ring until drained. 235 + while (dec_run) { 236 + if (!hls_streaming) { 237 + Sleep(10); 238 + } else { 239 + // Reset per-session pacing counters so the post-playback 240 + // summary reflects only this session's skip/display ratio. 241 + dec_sess_display_count = 0; 242 + dec_sess_skip_count = 0; 243 + DecodeDrainSession; 244 + hls_producer_done = FALSE; 245 + hls_streaming = FALSE; 246 + } 247 + } 248 + }
+257
holyc/vodbrowser/Menu.HC
··· 1 + // vodbrowser/Menu.HC — 2x3 tile grid UI with pagination 2 + // 3 + // Layout: header at top, 2 rows of 3 tiles, footer with arrow- 4 + // navigable buttons (Back / Fwd / Prefs / Quit). Each tile is a 5 + // plain ASCII box with the sanitised video title inside a DolDoc 6 + // marquee. The currently-selected slot — either a tile or a footer 7 + // button — is coloured red so the user knows what Enter will do. 8 + // 9 + // Pagination state is `menu_page_idx` (zero-based) plus `menu_sel_idx` 10 + // for the current selection. Selection indices [0..PAGE_SIZE) point 11 + // into the current page's tiles; indices MENU_SEL_PREV..MENU_SEL_EXIT 12 + // point at the footer buttons. 13 + // 14 + // Depends on: vodbrowser/Prefs.HC (MAX_VIDEOS_CAP), 15 + // vodbrowser/VideoList.HC (vid_titles, vid_count, vid_exhausted). 16 + 17 + #define PAGE_SIZE 6 18 + #define TILE_TITLE_W 20 19 + // Footer "selection indices" that live past the video tiles. Four 20 + // arrow-navigable buttons in a row: 21 + // [ < Prev ] [ Next > ] [ S: Settings ] [ Esc: Exit ] 22 + #define MENU_SEL_PREV (PAGE_SIZE + 0) 23 + #define MENU_SEL_NEXT (PAGE_SIZE + 1) 24 + #define MENU_SEL_SETTINGS (PAGE_SIZE + 2) 25 + #define MENU_SEL_EXIT (PAGE_SIZE + 3) 26 + 27 + I64 menu_page_idx; 28 + I64 menu_sel_idx; 29 + 30 + // Emit one line of a tile. `line` is 0..4 for the 5 rows that make up 31 + // a tile: 0 = top border, 1 = blank, 2 = title, 3 = blank, 4 = bottom 32 + // border. If `idx` is past the end of the video list we emit an empty 33 + // slot of matching width so the grid stays aligned. 34 + // 35 + // Long titles use the DolDoc marquee tag for horizontal scrolling 36 + // inside a 20-column cell. Titles that already fit render static. 37 + // We rewrite dangerous bytes (dollar, double-quote, backslash, and 38 + // control chars) as spaces before embedding the string into the 39 + // tag, so user text cannot inject broken markup and crash WinMgrs 40 + // render loop. 41 + U0 SanitizeTitle(U8 *dst, U8 *src, I64 max) 42 + { 43 + if (!dst || max <= 0) return; 44 + if (!src) { dst[0] = 0; return; } 45 + I64 i = 0; 46 + while (src[i] && i < max - 1) { 47 + I64 c = src[i]; 48 + I64 out = c; 49 + if (c == 0x24) out = ' '; 50 + if (c == 0x22) out = ' '; 51 + if (c == 0x5C) out = ' '; 52 + if (c < 0x20) out = ' '; 53 + dst[i] = out; 54 + i++; 55 + } 56 + dst[i] = 0; 57 + } 58 + 59 + U0 MenuTileLine(I64 idx, I64 line, Bool selected) 60 + { 61 + U8 *col_open = ""; 62 + U8 *col_close = ""; 63 + 64 + if (idx >= vid_count || idx < 0) { 65 + " "; 66 + return; 67 + } 68 + if (selected) { 69 + col_open = "$$FG,LTRED$$"; 70 + col_close = "$$FG$$"; 71 + } 72 + 73 + if (line == 0 || line == 4) { 74 + "%s+----------------------+%s ", col_open, col_close; 75 + } 76 + if (line == 1 || line == 3) { 77 + "%s| |%s ", col_open, col_close; 78 + } 79 + if (line == 2) { 80 + U8 safe[96]; 81 + SanitizeTitle(safe, vid_titles[idx], 96); 82 + "%s| $$TX,\"%s\",SCX=%d$$ |%s ", 83 + col_open, safe, TILE_TITLE_W, col_close; 84 + } 85 + } 86 + 87 + U0 RenderMenuPage() 88 + { 89 + // Reset the hit region table before emitting anything — we repopulate 90 + // it from scratch every frame so a page flip or paginate immediately 91 + // updates the click targets to match what's on screen. 92 + HitReset; 93 + DocClear; 94 + 95 + // BLUE is the default "heading" color on TempleOS's white DolDoc 96 + // background and gives good contrast. LTCYAN looked washed out. 97 + "\n$$FG,BLUE$$$$TX+CX,\"VodBrowser\"$$$$FG$$\n\n"; 98 + // Header emitted 3 newlines: the leading \n puts us on row 1, the 99 + // centered title occupies row 1, and the trailing \n\n leaves the 100 + // cursor at row 3 ready for the grid. 101 + g_render_row = 3; 102 + 103 + I64 num_pages = (vid_count + PAGE_SIZE - 1) / PAGE_SIZE; 104 + if (num_pages < 1) num_pages = 1; 105 + 106 + // Two rows of three tiles each. Each tile is 5 output lines tall. 107 + // We capture the row counter before and after each grid row so the 108 + // hit region for a tile spans all five of its rendered lines. 109 + I64 row, col, line; 110 + I64 page_start = menu_page_idx * PAGE_SIZE; 111 + for (row = 0; row < 2; row++) { 112 + I64 tile_row0 = g_render_row; 113 + for (line = 0; line < 5; line++) { 114 + " "; 115 + for (col = 0; col < 3; col++) { 116 + I64 idx = page_start + row * 3 + col; 117 + Bool sel = FALSE; 118 + if (idx < vid_count && (idx - page_start) == menu_sel_idx) sel = TRUE; 119 + MenuTileLine(idx, line, sel); 120 + } 121 + "\n"; 122 + g_render_row++; 123 + } 124 + I64 tile_row1 = g_render_row; 125 + 126 + // Record hit regions for this row of tiles. Tile columns start at 127 + // col 2 and are 24 cells wide with a 2-cell gap — see MenuTileLine 128 + // which prints "+<22 dashes>+ " per tile. Empty trailing slots on 129 + // the final page aren't recorded so clicking empty space does 130 + // nothing instead of playing a zero-title "video". 131 + // 132 + // NOTE: `hidx` / `cc` names deliberately avoid shadowing the `idx` 133 + // and `col` locals from the inner render loop above. HolyC's 134 + // single-pass compiler treats locals as function-scoped, not 135 + // block-scoped, so re-declaring `I64 idx` or `I64 col` in a 136 + // sibling block fails with "Duplicate member at =". 137 + I64 cc; 138 + for (cc = 0; cc < 3; cc++) { 139 + I64 hidx = page_start + row * 3 + cc; 140 + if (hidx < vid_count) { 141 + I64 c0 = 2 + cc * 26; 142 + HitAdd(c0, c0 + 24, tile_row0, tile_row1, row * 3 + cc); 143 + } 144 + } 145 + 146 + "\n"; 147 + g_render_row++; 148 + } 149 + 150 + // Dim, parenthesised key legend so the user can tell at a glance 151 + // that it's informational text, not another row of interactive 152 + // items. DKGRAY is readable on the default white DolDoc background 153 + // while still being visually subordinate to the main content — 154 + // LTGRAY was so washed-out on white it was effectively invisible. 155 + "\n$$FG,DKGRAY$$ (Arrows move . Click or Enter plays / activates)$$FG$$\n"; 156 + g_render_row += 2; // leading \n advances, trailing \n advances 157 + 158 + // Footer: arrow-navigable buttons. `[<Prev]` and `[Next>]` let the 159 + // user page forward/back without a dedicated keyboard shortcut — 160 + // Next triggers an on-demand fetch of the next batch from the 161 + // server once the loaded list runs out. Disabled buttons render 162 + // in gray; the currently-selected button renders in red. 163 + Bool can_prev = (menu_page_idx > 0); 164 + Bool can_next = FALSE; 165 + if (menu_page_idx + 1 < num_pages) can_next = TRUE; 166 + else if (!vid_exhausted && vid_count < MAX_VIDEOS_CAP) can_next = TRUE; 167 + 168 + U8 *prev_col = ""; 169 + U8 *prev_cclo = ""; 170 + U8 *next_col = ""; 171 + U8 *next_cclo = ""; 172 + U8 *set_col = ""; 173 + U8 *set_cclo = ""; 174 + U8 *exit_col = ""; 175 + U8 *exit_cclo = ""; 176 + 177 + if (menu_sel_idx == MENU_SEL_PREV) { 178 + prev_col = "$$FG,LTRED$$"; 179 + prev_cclo = "$$FG$$"; 180 + } else if (!can_prev) { 181 + prev_col = "$$FG,DKGRAY$$"; 182 + prev_cclo = "$$FG$$"; 183 + } 184 + if (menu_sel_idx == MENU_SEL_NEXT) { 185 + next_col = "$$FG,LTRED$$"; 186 + next_cclo = "$$FG$$"; 187 + } else if (!can_next) { 188 + next_col = "$$FG,DKGRAY$$"; 189 + next_cclo = "$$FG$$"; 190 + } 191 + if (menu_sel_idx == MENU_SEL_SETTINGS) { 192 + set_col = "$$FG,LTRED$$"; 193 + set_cclo = "$$FG$$"; 194 + } 195 + if (menu_sel_idx == MENU_SEL_EXIT) { 196 + exit_col = "$$FG,LTRED$$"; 197 + exit_cclo = "$$FG$$"; 198 + } 199 + 200 + // Footer button labels deliberately avoid words that match HolyC 201 + // kernel symbols. Default double-click on a DolDoc looks up the 202 + // word under the cursor and runs it if it resolves, so text like 203 + // Exit or Settings would literally run those functions and end 204 + // the program. Back / Fwd / Prefs / Quit are safe. 205 + // 206 + // Print the buttons one at a time and advance a column counter 207 + // (`cx`) in lockstep with the printed character widths so each 208 + // button's hit region exactly matches the text on screen — the 209 + // 2-char gaps between buttons are _not_ clickable, which means 210 + // clicks between buttons do nothing instead of activating a 211 + // neighbour. Widths: "[ < Back ]" = 10, "[ Fwd > ]" = 9, 212 + // "[ Prefs ]" = 9, "[ Quit ]" = 8. 213 + "\n "; 214 + g_render_row++; // advance past the leading \n 215 + I64 footer_row = g_render_row; 216 + I64 cx = 2; 217 + 218 + "%s[ < Back ]%s", prev_col, prev_cclo; 219 + HitAdd(cx, cx + 10, footer_row, footer_row + 1, MENU_SEL_PREV); 220 + " "; 221 + cx += 12; 222 + 223 + "%s[ Fwd > ]%s", next_col, next_cclo; 224 + HitAdd(cx, cx + 9, footer_row, footer_row + 1, MENU_SEL_NEXT); 225 + " "; 226 + cx += 11; 227 + 228 + "%s[ Prefs ]%s", set_col, set_cclo; 229 + HitAdd(cx, cx + 9, footer_row, footer_row + 1, MENU_SEL_SETTINGS); 230 + " "; 231 + cx += 11; 232 + 233 + "%s[ Quit ]%s\n", exit_col, exit_cclo; 234 + HitAdd(cx, cx + 8, footer_row, footer_row + 1, MENU_SEL_EXIT); 235 + g_render_row++; 236 + } 237 + 238 + // Clamp the selection into a valid state. Leaves any footer button 239 + // selection alone (Prev / Next / Prefs / Quit are always valid). 240 + // For in-grid selections, clamps to the last populated tile on the 241 + // current page so a short final page doesn't land on an empty slot. 242 + U0 MenuClampSel() 243 + { 244 + // Any footer slot (6..9) is always valid — don't touch. 245 + if (menu_sel_idx >= PAGE_SIZE) return; 246 + 247 + I64 last_on_page = vid_count - menu_page_idx * PAGE_SIZE - 1; 248 + if (last_on_page >= PAGE_SIZE) last_on_page = PAGE_SIZE - 1; 249 + if (last_on_page < 0) last_on_page = 0; 250 + if (menu_sel_idx > last_on_page) menu_sel_idx = last_on_page; 251 + if (menu_sel_idx < 0) menu_sel_idx = 0; 252 + } 253 + 254 + // Hit testing is now handled by the shared HitFind() in Util.HC — 255 + // RenderMenuPage populates g_hits[] as it walks the layout, so any 256 + // future change to the render geometry automatically updates the 257 + // click targets without a second copy of the row/col math here.
+464
holyc/vodbrowser/NetTask.HC
··· 1 + // vodbrowser/NetTask.HC — the background network worker task 2 + // 3 + // Runs on its own core (core 1 by default; see CorePolicyBuild). Owns 4 + // everything lwIP- and BearSSL-related because those aren't 5 + // reentrant — only this task may call NetFetch* / NetPoll. 6 + // 7 + // The task is message-driven: the main task sets `net_req_url` and 8 + // then posts a net_msg, NetTaskFn picks it up, does the work, and 9 + // writes the result into the net_resp_* globals. 10 + // 11 + // For HLS playback the task additionally runs a segment producer loop 12 + // that feeds the hls_ring ring buffer, which DecodeTask drains in 13 + // parallel — that overlap is what keeps playback smooth across 14 + // segment boundaries. 15 + // 16 + // Depends on: vodbrowser/Util.HC (AsyncFetch, BufFind, etc.), 17 + // vodbrowser/Prefs.HC (g_prefs reads). 18 + 19 + // ================================================================== 20 + // HLS variant table & parsing (used during master-playlist handling 21 + // below, so must be declared before NetTaskFn). The compile-time 22 + // constants bound these arrays and the segment byte-range arrays in 23 + // the FETCH_HLS case. 24 + // ================================================================== 25 + 26 + // Upper bound on how many fMP4 segments we will enumerate from a 27 + // single sub-playlist. The sub-playlist for a VOD contains all the 28 + // byte-ranges up front, so "enough to cover a full video" is ~1-2 29 + // per second; 2048 slots supports ~30 minutes. Dynamic alloc would 30 + // be cleaner but this is simple and costs 32KB (2x I64 arrays). 31 + #define HLS_MAX_SEGS 2048 32 + 33 + // Upper bound on discovered master-playlist variants. 34 + #define HLS_MAX_VARIANTS 16 35 + 36 + class CVariant { 37 + U8 url[2048]; 38 + I64 width; 39 + I64 height; 40 + I64 pixels; // width * height, or 0 if unknown 41 + }; 42 + 43 + CVariant hls_variants[HLS_MAX_VARIANTS]; 44 + I64 hls_num_variants; 45 + 46 + // Iterate every #EXT-X-STREAM-INF: entry in the master playlist body 47 + // and fill hls_variants[]. Returns variant count. Each variant captures 48 + // the following URL line and the RESOLUTION attribute (if present). 49 + I64 ParseMasterVariants(U8 *body, I64 bl) 50 + { 51 + hls_num_variants = 0; 52 + U8 *sp = body; 53 + U8 *end = body + bl; 54 + while (sp < end && hls_num_variants < HLS_MAX_VARIANTS) { 55 + U8 *si = BufFind(sp, end - sp, "#EXT-X-STREAM-INF:"); 56 + if (!si) break; 57 + 58 + // Scope the attribute search to just this STREAM-INF line 59 + U8 *eol = si; 60 + while (eol < end && *eol != '\n' && *eol != '\r') eol++; 61 + 62 + U8 res_val[32]; 63 + res_val[0] = 0; 64 + BufFindAttr(si, eol, "RESOLUTION=", res_val, 32); 65 + 66 + I64 w = 0, h = 0; 67 + if (res_val[0]) { 68 + // Parse "WIDTHxHEIGHT" 69 + U8 *xp = res_val; 70 + while (*xp >= '0' && *xp <= '9') { w = w * 10 + (*xp - '0'); xp++; } 71 + if (*xp == 'x' || *xp == 'X') { 72 + xp++; 73 + while (*xp >= '0' && *xp <= '9') { h = h * 10 + (*xp - '0'); xp++; } 74 + } 75 + } 76 + 77 + // URL is on the next line after the STREAM-INF line 78 + U8 *url_line = HlsNextLine(si, end); 79 + CopyLine(hls_variants[hls_num_variants].url, url_line, end, 2048); 80 + hls_variants[hls_num_variants].width = w; 81 + hls_variants[hls_num_variants].height = h; 82 + hls_variants[hls_num_variants].pixels = w * h; 83 + hls_num_variants++; 84 + 85 + sp = HlsNextLine(url_line, end); 86 + } 87 + return hls_num_variants; 88 + } 89 + 90 + // Pick a variant based on the user's Source Resolution preference. 91 + // target_pixels is display_w * display_h from the prefs, used for 92 + // the Auto case. Returns an index into hls_variants[], or 0 if the 93 + // playlist had no variants (caller must check hls_num_variants first). 94 + I64 PickVariant(I64 quality, I64 target_pixels) 95 + { 96 + if (hls_num_variants <= 0) return 0; 97 + I64 i, best; 98 + 99 + // SRC_Q_SOURCE: honour server order (first entry) 100 + if (quality == SRC_Q_SOURCE) return 0; 101 + 102 + // SRC_Q_HIGHEST: max w*h 103 + if (quality == SRC_Q_HIGHEST) { 104 + best = 0; 105 + for (i = 1; i < hls_num_variants; i++) 106 + if (hls_variants[i].pixels > hls_variants[best].pixels) best = i; 107 + return best; 108 + } 109 + 110 + // SRC_Q_LOWEST: min non-zero w*h (fall back to first if all are 0) 111 + if (quality == SRC_Q_LOWEST) { 112 + best = 0; 113 + for (i = 0; i < hls_num_variants; i++) 114 + if (hls_variants[i].pixels > 0) { best = i; break; } 115 + for (i = best + 1; i < hls_num_variants; i++) 116 + if (hls_variants[i].pixels > 0 && 117 + hls_variants[i].pixels < hls_variants[best].pixels) best = i; 118 + return best; 119 + } 120 + 121 + // SRC_Q_AUTO: smallest variant whose pixels >= target; fall back to 122 + // the highest available variant if nothing qualifies. 123 + if (quality == SRC_Q_AUTO) { 124 + if (target_pixels <= 0) { 125 + // Native display → pick the highest source 126 + best = 0; 127 + for (i = 1; i < hls_num_variants; i++) 128 + if (hls_variants[i].pixels > hls_variants[best].pixels) best = i; 129 + return best; 130 + } 131 + best = -1; 132 + for (i = 0; i < hls_num_variants; i++) { 133 + if (hls_variants[i].pixels >= target_pixels) { 134 + if (best < 0 || hls_variants[i].pixels < hls_variants[best].pixels) 135 + best = i; 136 + } 137 + } 138 + if (best >= 0) return best; 139 + // Nothing met the target — take the highest we have 140 + best = 0; 141 + for (i = 1; i < hls_num_variants; i++) 142 + if (hls_variants[i].pixels > hls_variants[best].pixels) best = i; 143 + return best; 144 + } 145 + 146 + return 0; 147 + } 148 + 149 + // ================================================================== 150 + // NetTask — runs on core 1, owns all lwIP + BearSSL 151 + // ================================================================== 152 + 153 + #define NET_MSG_NONE 0 154 + #define NET_MSG_FETCH 1 155 + #define NET_MSG_FETCH_HLS 2 156 + 157 + I64 net_run; 158 + I64 net_msg; 159 + I64 net_ready; 160 + U32 net_ip; 161 + U8 *net_req_url; 162 + I64 net_resp_done; 163 + I64 net_resp_rc; 164 + I64 net_resp_status; 165 + U8 *net_resp_body; 166 + I64 net_resp_body_len; 167 + 168 + // HLS streaming state. NetTask is the producer (downloads segments into 169 + // the ring), DecodeTask is the consumer (pops and decodes). Keeping a 170 + // small prefetch queue means decode never pauses waiting for the next 171 + // segment's HTTP fetch — as long as the producer stays ahead, playback 172 + // is smooth. 173 + // 174 + // Each slot is a PRE-ALLOCATED fixed-size buffer created once at 175 + // BrowseInit and reused across all segments for the lifetime of the 176 + // Browse session. We used to MAlloc/Free per segment, but that hit a 177 + // heap-header corruption bug inside TempleOS's Free somewhere — moving 178 + // to fixed slots sidesteps the issue entirely because we only call 179 + // MAlloc/Free at session boundaries. Slot size is the ceiling for 180 + // init+media segment bytes; anything larger than this is skipped with 181 + // a debug print. 182 + // 183 + // Size/count tuning: after the Apr 2026 decode optimisations, decode 184 + // speed is fast enough on 1080p60 that DecodeTask can drain the ring 185 + // faster than NetTask can refill it on a typical HTTPS link. Bumped 186 + // the ring from 3→6 slots to give the producer more headroom. Slot 187 + // size bumped from 4→8 MB because observed segment sizes (5 Mbps @ 188 + // 6 sec) sit around 3.8 MB and we want slack for longer segments. 189 + // Total RAM pre-allocated: 48 MB, fine on any VM. 190 + #define HLS_RING_SIZE 6 191 + #define HLS_RING_SLOT_SIZE (8 * 1024 * 1024) // 8 MB per slot 192 + 193 + I64 hls_streaming; // session active (set by NetTask, cleared by DecodeTask) 194 + I64 hls_first_ready; // first segment opened → PlayVideoStream can enter render mode 195 + I64 hls_producer_done; // NetTask has finished producing (ring may still drain) 196 + 197 + // Set by PlayVideoStream (main task) when the user presses ESC. All 198 + // three tasks check this flag and short-circuit: NetTask stops 199 + // producing new segments after the current fetch, DecodeTask exits 200 + // its drain loop and per-frame loop, and the main task's wait-for- 201 + // workers-to-finish step sees a prompt hls_streaming = FALSE. 202 + I64 pv_stop_requested; 203 + 204 + U8 *hls_ring[HLS_RING_SIZE]; // mini-fMP4 frags (init + media), owned by ring 205 + I64 hls_ring_size[HLS_RING_SIZE]; 206 + I64 hls_ring_head; // producer: next slot to fill 207 + I64 hls_ring_tail; // consumer: next slot to read 208 + I64 hls_ring_count; // slots currently full — producer waits if == HLS_RING_SIZE, 209 + // consumer waits if == 0. Aligned I64 R/W is atomic on x86_64 210 + // and that's all we need for eventually-consistent progress. 211 + 212 + U0 NetTaskFn(I64) 213 + { 214 + elf_quiet = TRUE; 215 + 216 + if (NetInit < 0) { 217 + StrPrint(net_dbg, "NetTask: init failed"); net_dbg_ready = TRUE; 218 + net_ready = TRUE; 219 + net_run = FALSE; 220 + return; 221 + } 222 + StrPrint(net_dbg, "NetTask: ready"); net_dbg_ready = TRUE; 223 + 224 + // DHCP 225 + net_ready = FALSE; 226 + net_ip = 0; 227 + I64 dp; 228 + for (dp = 0; dp < 300; dp++) { 229 + NetPoll; 230 + net_ip = NetGetIp; 231 + if (net_ip) break; 232 + Sleep(50); 233 + } 234 + net_ready = TRUE; 235 + 236 + // Message loop 237 + while (net_run) { 238 + NetPoll; 239 + 240 + switch (net_msg) { 241 + case NET_MSG_FETCH: 242 + { 243 + I64 rc = AsyncFetch(net_req_url, 244 + &net_resp_status, &net_resp_body, &net_resp_body_len); 245 + net_resp_rc = rc; 246 + net_msg = NET_MSG_NONE; 247 + net_resp_done = TRUE; 248 + } 249 + break; 250 + 251 + case NET_MSG_FETCH_HLS: 252 + { 253 + I64 hrc = -1; 254 + I64 st, bl; 255 + U8 *body; 256 + U8 *init_data = NULL; 257 + I64 init_size = 0; 258 + I64 si = 0; 259 + 260 + // Reset ring state before announcing the session. DecodeTask 261 + // watches hls_streaming and will jump in as soon as it sees 262 + // the flag flip, so all ring indices must be zeroed first. 263 + hls_ring_head = 0; 264 + hls_ring_tail = 0; 265 + hls_ring_count = 0; 266 + hls_producer_done = FALSE; 267 + hls_first_ready = FALSE; 268 + hls_streaming = TRUE; 269 + 270 + // 1. Fetch master playlist from the VOD XRPC endpoint. 271 + U8 enc_uri[512], purl[2048]; 272 + UrlEncode(enc_uri, net_req_url, 512); 273 + StrPrint(purl, "%splace.stream.playback.getVideoPlaylist?uri=%s", 274 + g_prefs.vod_base, enc_uri); 275 + StrPrint(net_dbg, "HLS: master playlist..."); net_dbg_ready = TRUE; 276 + 277 + hrc = AsyncFetch(purl, &st, &body, &bl); 278 + if (hrc != 0) { 279 + StrPrint(net_dbg, "HLS: master fetch failed %d", hrc); 280 + net_dbg_ready = TRUE; 281 + goto hls_done; 282 + } 283 + 284 + // 2. Enumerate every STREAM-INF variant and pick one based on 285 + // the user's Source Resolution preference. 286 + ParseMasterVariants(body, bl); 287 + if (hls_num_variants <= 0) { 288 + StrPrint(net_dbg, "HLS: no STREAM-INF"); net_dbg_ready = TRUE; 289 + Free(body); hrc = -200; goto hls_done; 290 + } 291 + I64 target_px = g_prefs.disp_w * g_prefs.disp_h; 292 + I64 pick = PickVariant(g_prefs.src_quality, target_px); 293 + StrPrint(net_dbg, "HLS: variant %d/%d (%dx%d)", 294 + pick + 1, hls_num_variants, 295 + hls_variants[pick].width, hls_variants[pick].height); 296 + net_dbg_ready = TRUE; 297 + 298 + U8 track_url[2048]; 299 + SafeStrCpy(track_url, hls_variants[pick].url, 2048); 300 + Free(body); 301 + 302 + if (track_url[0] == 'h') StrPrint(purl, "%s", track_url); 303 + else StrPrint(purl, "%s%s", g_prefs.vod_base, track_url); 304 + 305 + // 3. Fetch sub-playlist. 306 + StrPrint(net_dbg, "HLS: sub-playlist..."); net_dbg_ready = TRUE; 307 + hrc = AsyncFetch(purl, &st, &body, &bl); 308 + if (hrc != 0) { 309 + StrPrint(net_dbg, "HLS: sub fetch failed %d", hrc); 310 + net_dbg_ready = TRUE; 311 + goto hls_done; 312 + } 313 + 314 + // 4. Parse EXT-X-MAP init URL + EXT-X-BYTERANGE segment list. 315 + U8 *map_line = BufFind(body, bl, "#EXT-X-MAP:URI=\""); 316 + if (!map_line) { 317 + StrPrint(net_dbg, "HLS: no EXT-X-MAP"); net_dbg_ready = TRUE; 318 + Free(body); hrc = -400; goto hls_done; 319 + } 320 + U8 *uri_start = map_line + 16; 321 + U8 init_url[2048]; 322 + I64 ui = 0; 323 + while (uri_start + ui < body + bl && uri_start[ui] != '"' && ui < 2047) { 324 + init_url[ui] = uri_start[ui]; ui++; 325 + } 326 + init_url[ui] = 0; 327 + 328 + I64 seg_off[HLS_MAX_SEGS], seg_len[HLS_MAX_SEGS]; 329 + U8 cdn_url[512]; 330 + I64 num_segs = 0; 331 + cdn_url[0] = 0; 332 + U8 *sp = body; 333 + U8 *send = body + bl; 334 + while (sp < send && num_segs < HLS_MAX_SEGS) { 335 + U8 *br_line = BufFind(sp, send - sp, "#EXT-X-BYTERANGE:"); 336 + if (!br_line) break; 337 + U8 *nums = br_line + 17; 338 + I64 slen = ParseNum(nums, send); 339 + while (nums < send && *nums >= '0' && *nums <= '9') nums++; 340 + I64 soff = 0; 341 + if (nums < send && *nums == '@') { nums++; soff = ParseNum(nums, send); } 342 + U8 *nxt = HlsNextLine(br_line, send); 343 + if (cdn_url[0] == 0 && nxt < send && nxt[0] == 'h') 344 + CopyLine(cdn_url, nxt, send, 512); 345 + seg_off[num_segs] = soff; 346 + seg_len[num_segs] = slen; 347 + num_segs++; 348 + sp = nxt; 349 + } 350 + Free(body); 351 + 352 + if (num_segs == 0 || cdn_url[0] == 0) { 353 + StrPrint(net_dbg, "HLS: no segments"); net_dbg_ready = TRUE; 354 + hrc = -400; goto hls_done; 355 + } 356 + StrPrint(net_dbg, "HLS: %d segs, streaming...", num_segs); 357 + net_dbg_ready = TRUE; 358 + 359 + // 5. Fetch init segment (kept resident; prepended to each fragment). 360 + if (init_url[0] == 'h') StrPrint(purl, "%s", init_url); 361 + else StrPrint(purl, "%s%s", g_prefs.vod_base, init_url); 362 + hrc = AsyncFetch(purl, &st, &body, &bl); 363 + if (hrc != 0) { 364 + StrPrint(net_dbg, "HLS: init fetch failed %d", hrc); 365 + net_dbg_ready = TRUE; 366 + goto hls_done; 367 + } 368 + init_data = MAlloc(bl); 369 + MemCpy(init_data, body, bl); 370 + init_size = bl; 371 + Free(body); 372 + 373 + // 6. Produce segments into the ring. DecodeTask consumes in 374 + // parallel, so downloads and decodes overlap — playback no 375 + // longer pauses between segments as long as the producer 376 + // keeps at least one slot ahead. 377 + for (si = 0; si < num_segs; si++) { 378 + if (pv_stop_requested) break; 379 + 380 + I64 t_fetch0 = cnts.jiffies; 381 + I64 ring_at_start = hls_ring_count; 382 + 383 + hrc = AsyncFetchRange(cdn_url, seg_off[si], 384 + seg_off[si] + seg_len[si] - 1, 385 + &st, &body, &bl); 386 + 387 + // Capture reuse bit RIGHT after the fetch — NetGetLastReused 388 + // returns the flag that was set when net_fetch_begin chose 389 + // its path. F = fresh connect, R = reused existing TLS. 390 + I64 reused = NetGetLastReused; 391 + U8 *rtag = "F"; 392 + if (reused) rtag = "R"; 393 + 394 + // Compact one-liner per fetch so more fits on-screen during 395 + // diagnosis. Shows reuse tag, seg index, bytes, wall time, 396 + // effective throughput, and the ring depth at start/end of 397 + // the fetch (the arrow shows whether the producer gained 398 + // or lost headroom relative to the consumer). 399 + I64 fetch_ms = (cnts.jiffies - t_fetch0) * 1000 / JIFFY_FREQ; 400 + I64 fetch_kbps = 0; 401 + if (fetch_ms > 0) fetch_kbps = (bl * 8) / fetch_ms; 402 + StrPrint(net_dbg, "%s s%d %dK %dms %dkbps r%d->%d", 403 + rtag, si + 1, bl / 1024, fetch_ms, fetch_kbps, 404 + ring_at_start, hls_ring_count); 405 + net_dbg_ready = TRUE; 406 + 407 + if (hrc != 0) { 408 + StrPrint(net_dbg, "s%d FAIL %d", si + 1, hrc); 409 + net_dbg_ready = TRUE; 410 + break; 411 + } 412 + 413 + // Size check: the pre-allocated slots are fixed-size, so 414 + // any segment that won't fit is skipped. For VOD streams 415 + // with the presets we target this shouldn't happen. 416 + I64 fsz = init_size + bl; 417 + if (fsz > HLS_RING_SLOT_SIZE) { 418 + StrPrint(net_dbg, "seg %d too big: %d > %d", si + 1, 419 + fsz, HLS_RING_SLOT_SIZE); 420 + net_dbg_ready = TRUE; 421 + Free(body); 422 + } else { 423 + // Wait until the ring has a free slot before touching 424 + // it. A "free" slot at head is guaranteed when count < 425 + // HLS_RING_SIZE because the consumer decrements count 426 + // only after it's done with the tail slot. 427 + while (hls_ring_count >= HLS_RING_SIZE && net_run) Sleep(5); 428 + if (net_run) { 429 + // Copy init + media segment into the pre-allocated 430 + // slot. No MAlloc, no Free — just a memcpy. 431 + U8 *slot = hls_ring[hls_ring_head]; 432 + MemCpy(slot, init_data, init_size); 433 + MemCpy(slot + init_size, body, bl); 434 + Free(body); 435 + 436 + hls_ring_size[hls_ring_head] = fsz; 437 + hls_ring_head = (hls_ring_head + 1) % HLS_RING_SIZE; 438 + hls_ring_count++; 439 + } else { 440 + Free(body); 441 + } 442 + } 443 + } 444 + 445 + hrc = 0; 446 + 447 + hls_done: 448 + if (init_data) Free(init_data); 449 + // Tell the consumer there'll be no more segments. DecodeTask 450 + // keeps draining whatever is already in the ring, then tears 451 + // itself down and clears hls_streaming. 452 + hls_producer_done = TRUE; 453 + net_resp_rc = hrc; 454 + net_msg = NET_MSG_NONE; 455 + net_resp_done = TRUE; 456 + } 457 + break; 458 + 459 + default: 460 + break; 461 + } 462 + Sleep(5); 463 + } 464 + }
+234
holyc/vodbrowser/Playback.HC
··· 1 + // vodbrowser/Playback.HC — full-screen HLS playback driven by WinMgr 2 + // 3 + // PvDrawIt is registered as Fs->draw_it while PlayVideoStream is 4 + // running; WinMgr calls it every frame on the main task's rendering 5 + // context. It copies the latest decoded palette frame from 6 + // dec_frame_pal into the DC body — no Media.prg thunk calls from 7 + // this task, so there's no thunk race with DecodeTask. 8 + // 9 + // PlayVideoStream kicks NetTask into HLS streaming mode, waits for 10 + // DecodeTask to produce the first frame, pushes the UI into 11 + // fullscreen/no-cursor mode via SettingsPush, then sits in an input 12 + // loop scanning for ESC. On exit it restores settings via SettingsPop 13 + // (which WinMgr sees and automatically repaints the window as normal 14 + // text). 15 + // 16 + // Depends on: vodbrowser/DecodeTask.HC (dec_* globals), 17 + // vodbrowser/NetTask.HC (hls_*, net_msg, pv_stop_requested). 18 + 19 + // FPS sample ring for the fps_log_interval pref. Declared at file 20 + // scope because HolyC's #define + stack-array combination inside a 21 + // function body behaved unreliably in earlier testing (samples were 22 + // never captured). File-scope arrays are unambiguous. 23 + #define FPS_SAMPLE_CAP 128 24 + I64 g_fps_ms_samples[FPS_SAMPLE_CAP]; 25 + I64 g_fps_fc_samples[FPS_SAMPLE_CAP]; 26 + I64 g_fps_n_samples; 27 + 28 + // Draw callback registered on the main task while video is playing. 29 + // WinMgr invokes this each frame from Fs's rendering context. MUST be 30 + // defined before PlayVideoStream (HolyC is single-pass). The CTask* 31 + // parameter is unused — we centre on the dc's own dimensions — so it 32 + // is declared without a name to silence the unused-var warning. 33 + U0 PvDrawIt(CTask *, CDC *dc) 34 + { 35 + if (!dec_frame_pal) return; 36 + if (dec_frame_w <= 0 || dec_frame_h <= 0) return; 37 + if (!dc || !dc->body) return; 38 + 39 + I64 dw = dec_frame_w, dh = dec_frame_h; 40 + I64 x = (dc->width - dw) / 2; 41 + I64 y = (dc->height - dh) / 2; 42 + if (x < 0) x = 0; 43 + if (y < 0) y = 0; 44 + 45 + I64 w = dw, h = dh; 46 + if (x + w > dc->width) w = dc->width - x; 47 + if (y + h > dc->height) h = dc->height - y; 48 + if (w <= 0 || h <= 0) return; 49 + 50 + I64 stride = dc->width_internal; 51 + I64 yy; 52 + for (yy = 0; yy < h; yy++) { 53 + MemCpy(dc->body + (y + yy) * stride + x, 54 + dec_frame_pal + yy * dw, 55 + w); 56 + } 57 + } 58 + 59 + U0 PlayVideoStream(U8 *at_uri, U8 *title) 60 + { 61 + "\nPlaying: %s\n", title; 62 + 63 + // Reset session flags BEFORE kicking off NetTask. pv_stop_requested 64 + // in particular must be cleared here: NetTask's segment-fetch loop 65 + // checks it and bails out immediately if it's still TRUE from a 66 + // previous ESC-aborted playback, which would leave dec_frame_seq=0 67 + // and trigger the "No frames decoded" fallback. 68 + pv_stop_requested = FALSE; 69 + dec_frame_seq = 0; 70 + dec_eof = FALSE; 71 + hls_streaming = FALSE; 72 + hls_first_ready = FALSE; 73 + // Defensive reset — HolyC globals persist across #include, and if a 74 + // prior run crashed between "print summary" and "reset counter" the 75 + // stale value would leak into this run's summary line. This is the 76 + // root cause of the nonsense "3421769 segment opens failed" count. 77 + dec_open_fails = 0; 78 + 79 + // Kick off streaming in NetTask 80 + net_req_url = at_uri; 81 + net_resp_done = FALSE; 82 + net_msg = NET_MSG_FETCH_HLS; 83 + 84 + // Wait for first segment to reach DecodeTask 85 + while (!hls_first_ready && !net_resp_done) { 86 + DrainDebug; 87 + Sleep(50); 88 + } 89 + DrainDebug; 90 + 91 + if (net_resp_done && net_resp_rc != 0) { 92 + "\nStreaming failed: %d\n", net_resp_rc; 93 + return; 94 + } 95 + 96 + // Wait for first decoded frame (so dec_vw/dec_vh are valid) 97 + while (dec_frame_seq == 0 && hls_streaming) { 98 + DrainDebug; 99 + Sleep(20); 100 + } 101 + if (dec_frame_seq == 0) { 102 + "\nNo frames decoded\n"; 103 + return; 104 + } 105 + 106 + "Playing %dx%d\n", dec_vw, dec_vh; 107 + FlushMsgs; // drop any pending key events from menu selection 108 + 109 + // Enter graphics mode. SettingsPop at the end restores border/ 110 + // cursor/size and clears draw_it, which triggers WinMgr to repaint 111 + // the window as normal text — automatic cleanup. 112 + SettingsPush; 113 + WinBorder; // toggle border off 114 + WinMax; // full-screen 115 + DocCursor; // hide cursor 116 + DocClear; // clear the doc so no menu text shows through 117 + Fs->draw_it = &PvDrawIt; 118 + 119 + I64 fc = 0; 120 + I64 last_seq = 0; 121 + I64 t0 = cnts.jiffies; 122 + I64 ch, sc; 123 + 124 + // fps sample ring. While in fullscreen/draw_it mode any printf to 125 + // the DolDoc is invisible (the DC body is overwritten every frame by 126 + // PvDrawIt). So instead of printing in real time we capture 127 + // (ms, frame_count) pairs into a file-scope array at the user- 128 + // configured interval, and print the whole series after SettingsPop 129 + // restores normal rendering. User sees their fps history as soon as 130 + // they press ESC. 131 + g_fps_n_samples = 0; 132 + I64 fps_interval_ms = g_prefs.fps_log_interval * 1000; 133 + I64 next_sample_ms = fps_interval_ms; // first sample at t=interval 134 + 135 + // Main task just waits; WinMgr drives rendering via PvDrawIt. 136 + // 137 + // IMPORTANT: do NOT call any Media.prg thunk from here. The thunk 138 + // uses global argument registers that DecodeTask is actively 139 + // writing — a concurrent call from this task would race and make 140 + // Browse run the wrong C function on its own stack. 141 + // 142 + // Exit detection: ScanKey() polls the task's message queue for a 143 + // key press (the canonical TempleOS pattern from BlackDiamond.HC). 144 + // FlushMsgs is called INSIDE the `if (ScanKey)` branch — matching 145 + // Terry's pattern exactly. Calling it outside every iteration was 146 + // draining the queue before ScanKey could observe pending keys. 147 + // 148 + // On ESC we set pv_stop_requested so NetTask + DecodeTask exit 149 + // their own loops promptly, then break out of this wait loop. 150 + while (hls_streaming || dec_frame_seq > last_seq) { 151 + if (dec_frame_seq > last_seq) { 152 + last_seq = dec_frame_seq; 153 + fc++; 154 + } 155 + 156 + if (fps_interval_ms > 0 && g_fps_n_samples < FPS_SAMPLE_CAP) { 157 + I64 ems = (cnts.jiffies - t0) * 1000 / JIFFY_FREQ; 158 + if (ems >= next_sample_ms) { 159 + g_fps_ms_samples[g_fps_n_samples] = ems; 160 + g_fps_fc_samples[g_fps_n_samples] = fc; 161 + g_fps_n_samples++; 162 + next_sample_ms += fps_interval_ms; 163 + } 164 + } 165 + 166 + if (ScanKey(&ch, &sc)) { 167 + if (ch == CH_ESC || ch == CH_SHIFT_ESC) { 168 + pv_stop_requested = TRUE; 169 + break; 170 + } 171 + FlushMsgs; 172 + } 173 + 174 + DrainDebug; 175 + Sleep(10); 176 + } 177 + 178 + // Drain any final NetTask completion state. DecodeTask clears 179 + // hls_streaming at the end of its session loop, so waiting on both 180 + // flags here guarantees both worker tasks are fully idle before 181 + // we return control to the menu. 182 + while (!net_resp_done) { DrainDebug; Sleep(50); } 183 + while (hls_streaming) { DrainDebug; Sleep(20); } 184 + DrainDebug; 185 + 186 + SettingsPop; 187 + 188 + // fps interval samples. Always print a status line telling the user 189 + // what the sampler saw, even if no samples were captured — that's 190 + // the fastest way to diagnose why the feature "doesn't work" (wrong 191 + // pref, playback too short, etc.). 192 + "\nfps-log: interval_ms=%d samples=%d pref=%d\n", 193 + fps_interval_ms, g_fps_n_samples, g_prefs.fps_log_interval; 194 + if (fps_interval_ms > 0 && g_fps_n_samples > 0) { 195 + "fps samples (every %ds):\n", g_prefs.fps_log_interval; 196 + I64 i; 197 + for (i = 0; i < g_fps_n_samples; i++) { 198 + I64 sample_fps = 0; 199 + if (g_fps_ms_samples[i] > 0) 200 + sample_fps = g_fps_fc_samples[i] * 1000 / g_fps_ms_samples[i]; 201 + " %ds: %d frames (%d fps)\n", 202 + g_fps_ms_samples[i] / 1000, g_fps_fc_samples[i], sample_fps; 203 + } 204 + } 205 + 206 + I64 ms = (cnts.jiffies - t0) * 1000 / JIFFY_FREQ; 207 + "\n%d frames in %dms", fc, ms; 208 + if (ms > 0) " (%d fps)", fc * 1000 / ms; 209 + if (dec_open_fails > 0) " [%d segment opens failed]", dec_open_fails; 210 + "\n"; 211 + dec_open_fails = 0; 212 + 213 + // Frame pacing summary. `displayed` is frames that decoded and 214 + // made it to screen; `skipped` is non-reference frames the C-side 215 + // pacing logic dropped dynamically (only when behind wallclock). 216 + I64 paced_total = dec_sess_display_count + dec_sess_skip_count; 217 + if (paced_total > 0) { 218 + "paced: %d displayed, %d skipped", 219 + dec_sess_display_count, dec_sess_skip_count; 220 + if (dec_sess_skip_count > 0) 221 + " (%d%% kept)", dec_sess_display_count * 100 / paced_total; 222 + "\n"; 223 + } 224 + 225 + // Hold the fps output on screen until the user acks. Without this 226 + // PressAKey, the next iteration of VodBrowser()'s main loop calls 227 + // RenderMenuPage, which DocClears the entire doc — erasing every 228 + // line we just printed. We only pause when fps logging is actually 229 + // on so ordinary playback→menu return still feels snappy. 230 + if (fps_interval_ms > 0) { 231 + "\nPress any key to return to the menu...\n"; 232 + PressAKey; 233 + } 234 + }
+297
holyc/vodbrowser/Prefs.HC
··· 1 + // vodbrowser/Prefs.HC — persistent user preferences 2 + // 3 + // `CPrefs` holds every user-visible knob: display resolution, 4 + // account (PDS / DID / VOD server), video fetch count, multicore 5 + // policy, and so on. The file `C:/VodBrowserPrefs.txt` is the 6 + // canonical on-disk format (plain `key=value` lines). 7 + // 8 + // `CCorePolicy` is the derived core-assignment used by BrowseInit to 9 + // spawn NetTask and DecodeTask on the right cores. 10 + // 11 + // Depends on: vodbrowser/Util.HC (SafeStrCpy, g_debug_msgs). 12 + 13 + // Hard ceiling on video list size. video_fetch_count is clamped to 14 + // this. At 16 bytes per entry (two pointers) the static arrays cost 15 + // 3.2KB at 200 slots, which is fine. ATProto servers typically cap 16 + // a single listRecords request at 100, so FetchVideos loops using 17 + // the returned cursor until it has `want` videos or the server 18 + // runs out. 19 + #define MAX_VIDEOS_CAP 200 20 + // Per-request ceiling enforced by the ATProto listRecords spec. 21 + #define ATPROTO_PAGE_SIZE 100 22 + 23 + // Source quality enum (mapped to menu option indices 1..4) 24 + #define SRC_Q_SOURCE 0 25 + #define SRC_Q_HIGHEST 1 26 + #define SRC_Q_LOWEST 2 27 + #define SRC_Q_AUTO 3 28 + 29 + // Multicore mode sentinels 30 + #define MC_AUTO -1 31 + #define MC_OFF 0 32 + 33 + // Dither mode values stored in CPrefs.dither_mode. Must match the 34 + // MEDIA_DITHER_* constants in media_api.h / Media.HC (DITHER_FLOYD etc). 35 + #define DITHER_MODE_FLOYD 0 36 + #define DITHER_MODE_NEAREST 1 37 + #define DITHER_MODE_GRAY 2 38 + 39 + // Relative path, intentionally. An absolute path with a drive letter 40 + // goes through Let2Drv → DrvChk and HARD-PANICS if the drive letter 41 + // isn't mounted — this hit us on the live-CD boot because there's 42 + // no C: when TempleOS is running directly from the ISO. A relative 43 + // path is resolved against the calling task's cur_dir, which is 44 + // guaranteed to exist (it's the drive we booted from), so FileRead 45 + // gracefully returns NULL for "no such file" instead of taking down 46 + // the whole task. The file ends up next to VodBrowser.HC: 47 + // - dev: T:/VodBrowserPrefs.txt (sources.iso, Cd("T:/")) 48 + // - distro live CD: T:/VodBrowser/VodBrowserPrefs.txt 49 + // - installed to HD: whichever Home dir VodBrowser was unpacked to 50 + // On a read-only medium PrefsSave still fails (and is swallowed by 51 + // its own try/catch in PrefsSave) which is fine — in-memory defaults 52 + // keep working. 53 + #define PREFS_PATH "VodBrowserPrefs.txt" 54 + 55 + class CPrefs { 56 + // Display 57 + I64 disp_w; // 0=Native 58 + I64 disp_h; 59 + // Source quality selection from HLS master playlist 60 + I64 src_quality; // SRC_Q_* 61 + // Video dither mode — DITHER_MODE_FLOYD / NEAREST / GRAY 62 + I64 dither_mode; 63 + // Source account 64 + U8 pds[256]; 65 + U8 did[160]; 66 + U8 vod_base[256]; 67 + // Misc 68 + I64 video_fetch_count; // clamped to [1, MAX_VIDEOS_CAP] 69 + I64 multicore; // MC_OFF, MC_AUTO, or explicit N in [2..mp_cnt] 70 + I64 debug_msgs; // TRUE = print worker debug channels to console 71 + I64 fps_log_interval; // seconds between fps samples during playback 72 + // (0 = off); samples are printed as a series 73 + // after playback ends 74 + I64 skip_non_ref; // TRUE = drop non-reference H.264 frames before 75 + // decode. The main lever for matching wallclock 76 + // to source duration on slow decode. 77 + }; 78 + 79 + // Collection is fixed — the whole app is built around this record type. 80 + #define ATPROTO_COL "place.stream.video" 81 + 82 + CPrefs g_prefs; // live, last-saved state 83 + CPrefs g_prefs_edit; // working copy used by Settings menu 84 + I64 g_prefs_dirty; // TRUE if g_prefs_edit differs from g_prefs 85 + 86 + // Core assignment policy — populated from g_prefs.multicore at BrowseInit 87 + // time, then consumed by the two Spawn calls. A tiny helper struct so 88 + // adding new worker tasks later doesn't require another round of 89 + // if/else branching on core count. 90 + class CCorePolicy { 91 + I64 net_core; 92 + I64 dec_core; 93 + }; 94 + 95 + CCorePolicy g_core_policy; 96 + 97 + U0 PrefsDefaults(CPrefs *p) 98 + { 99 + MemSet(p, 0, sizeof(CPrefs)); 100 + p->disp_w = 400; 101 + p->disp_h = 225; 102 + p->src_quality = SRC_Q_SOURCE; 103 + p->dither_mode = DITHER_MODE_FLOYD; 104 + SafeStrCpy(p->pds, "https://iameli.com", 256); 105 + SafeStrCpy(p->did, "did:plc:rbvrr34edl5ddpuwcubjiost", 160); 106 + SafeStrCpy(p->vod_base, "https://vod-beta.stream.place/xrpc/", 256); 107 + p->video_fetch_count = 6; 108 + p->multicore = MC_AUTO; 109 + p->debug_msgs = FALSE; 110 + p->fps_log_interval = 0; 111 + p->skip_non_ref = FALSE; 112 + } 113 + 114 + // Dispatch a key=value pair into the right field. Unknown keys are 115 + // silently ignored (forward/backward compat). Both `key` and `val` 116 + // must be null-terminated; the caller does that by copying into 117 + // fixed buffers and zero-terminating. 118 + U0 PrefsSetKey(CPrefs *p, U8 *key, U8 *val) 119 + { 120 + if (!StrCmp(key, "disp_w")) p->disp_w = Str2I64(val); 121 + if (!StrCmp(key, "disp_h")) p->disp_h = Str2I64(val); 122 + if (!StrCmp(key, "src_quality")) p->src_quality = Str2I64(val); 123 + if (!StrCmp(key, "dither_mode")) p->dither_mode = Str2I64(val); 124 + if (!StrCmp(key, "pds")) SafeStrCpy(p->pds, val, 256); 125 + if (!StrCmp(key, "did")) SafeStrCpy(p->did, val, 160); 126 + if (!StrCmp(key, "vod_base")) SafeStrCpy(p->vod_base, val, 256); 127 + if (!StrCmp(key, "video_fetch_count")) p->video_fetch_count = Str2I64(val); 128 + if (!StrCmp(key, "multicore")) p->multicore = Str2I64(val); 129 + if (!StrCmp(key, "debug_msgs")) p->debug_msgs = Str2I64(val); 130 + if (!StrCmp(key, "fps_log_interval")) p->fps_log_interval = Str2I64(val); 131 + if (!StrCmp(key, "skip_non_ref")) p->skip_non_ref = Str2I64(val); 132 + } 133 + 134 + // Parse the prefs file. Returns 0 on success, -1 if file missing or 135 + // the drive in `path` isn't mounted (e.g. running from a live CD 136 + // with no C:). Layout: `key=value` one per line. `#` starts a 137 + // comment. Leading whitespace is tolerated. Values may contain 138 + // spaces and any non-newline character up to end of line (so URLs 139 + // with `=` won't survive, but none of ours have `=` in them). 140 + I64 PrefsLoad(CPrefs *p, U8 *path) 141 + { 142 + // Skip the FileRead entirely if the file isn't there. FileRead's 143 + // internal "file not found" logging spams the doc with a big 144 + // `&FileRead &PrefsLoad &BrowseInit &VodBrowser ERROR: ...` chain 145 + // even when the caller handles the NULL return cleanly. FileFind 146 + // is the quiet way to ask the same question. We still wrap the 147 + // FileRead in try/catch as a belt-and-suspenders guard — on 148 + // read-only media where the path's drive letter isn't mounted, 149 + // FileRead can still panic past FileFind's check. 150 + if (!FileFind(path)) return -1; 151 + 152 + I64 size = 0; 153 + U8 *data = NULL; 154 + try { 155 + data = FileRead(path, &size); 156 + } 157 + catch { 158 + data = NULL; 159 + } 160 + if (!data || size <= 0) { 161 + if (data) Free(data); 162 + return -1; 163 + } 164 + 165 + U8 key[64], val[512]; 166 + U8 *sp = data; 167 + U8 *end = data + size; 168 + while (sp < end) { 169 + // Skip leading whitespace on this line 170 + while (sp < end && (*sp == ' ' || *sp == '\t')) sp++; 171 + // Find end of line 172 + U8 *eol = sp; 173 + while (eol < end && *eol != '\n' && *eol != '\r') eol++; 174 + 175 + // Non-blank, non-comment? 176 + if (sp < eol && *sp != '#') { 177 + // Find '=' within [sp, eol) 178 + U8 *eq = sp; 179 + while (eq < eol && *eq != '=') eq++; 180 + if (eq < eol) { 181 + // Copy key [sp, eq) trimmed of trailing spaces 182 + U8 *ke = eq; 183 + while (ke > sp && (ke[-1] == ' ' || ke[-1] == '\t')) ke--; 184 + I64 klen = ke - sp; 185 + if (klen > 63) klen = 63; 186 + MemCpy(key, sp, klen); key[klen] = 0; 187 + 188 + // Copy value [eq+1, eol) trimmed on both sides 189 + U8 *vs = eq + 1; 190 + while (vs < eol && (*vs == ' ' || *vs == '\t')) vs++; 191 + U8 *ve = eol; 192 + while (ve > vs && (ve[-1] == ' ' || ve[-1] == '\t')) ve--; 193 + I64 vlen = ve - vs; 194 + if (vlen > 511) vlen = 511; 195 + if (vlen < 0) vlen = 0; 196 + MemCpy(val, vs, vlen); val[vlen] = 0; 197 + 198 + PrefsSetKey(p, key, val); 199 + } 200 + } 201 + 202 + // Advance past the line terminator 203 + sp = eol; 204 + if (sp < end && *sp == '\r') sp++; 205 + if (sp < end && *sp == '\n') sp++; 206 + } 207 + 208 + Free(data); 209 + return 0; 210 + } 211 + 212 + // Serialize prefs to disk. Overwrites any existing file. Returns 213 + // bytes written (>=0) or a negative error code. 214 + // 215 + // StrPrint writes a null-terminated string at `buf + len`; we advance 216 + // `len` by the length of whatever it just wrote via StrLen. That's 217 + // the idiomatic way to chain StrPrints in HolyC since the function 218 + // returns a pointer rather than a byte count. 219 + I64 PrefsSave(CPrefs *p, U8 *path) 220 + { 221 + U8 *buf = MAlloc(4096); 222 + if (!buf) return -1; 223 + I64 len = 0; 224 + StrPrint(buf + len, 225 + "# VodBrowser prefs\n" 226 + "# Auto-generated. Delete this file to reset to defaults.\n\n"); 227 + len += StrLen(buf + len); 228 + StrPrint(buf + len, "disp_w=%d\n", p->disp_w); len += StrLen(buf + len); 229 + StrPrint(buf + len, "disp_h=%d\n", p->disp_h); len += StrLen(buf + len); 230 + StrPrint(buf + len, "src_quality=%d\n", p->src_quality); len += StrLen(buf + len); 231 + StrPrint(buf + len, "dither_mode=%d\n", p->dither_mode); len += StrLen(buf + len); 232 + StrPrint(buf + len, "pds=%s\n", p->pds); len += StrLen(buf + len); 233 + StrPrint(buf + len, "did=%s\n", p->did); len += StrLen(buf + len); 234 + StrPrint(buf + len, "vod_base=%s\n", p->vod_base); len += StrLen(buf + len); 235 + StrPrint(buf + len, "video_fetch_count=%d\n", p->video_fetch_count); len += StrLen(buf + len); 236 + StrPrint(buf + len, "multicore=%d\n", p->multicore); len += StrLen(buf + len); 237 + StrPrint(buf + len, "debug_msgs=%d\n", p->debug_msgs); len += StrLen(buf + len); 238 + StrPrint(buf + len, "fps_log_interval=%d\n", p->fps_log_interval); len += StrLen(buf + len); 239 + StrPrint(buf + len, "skip_non_ref=%d\n", p->skip_non_ref); len += StrLen(buf + len); 240 + // Same DrvChk hazard as PrefsLoad — on a read-only medium or when 241 + // the target drive isn't mounted, FileWrite will panic. Catch it 242 + // silently and return an error; the caller doesn't need to do 243 + // anything special since we already have the defaults in memory. 244 + I64 rc = -1; 245 + try { 246 + rc = FileWrite(path, buf, len); 247 + } 248 + catch { 249 + rc = -1; 250 + } 251 + Free(buf); 252 + return rc; 253 + } 254 + 255 + // Clamp out-of-range values and push prefs into runtime consumers 256 + // that need immediate updates. Called after every Save and once at 257 + // startup after PrefsLoad. 258 + U0 PrefsApply(CPrefs *p) 259 + { 260 + // Clamp video_fetch_count 261 + if (p->video_fetch_count < 1) p->video_fetch_count = 1; 262 + if (p->video_fetch_count > MAX_VIDEOS_CAP) p->video_fetch_count = MAX_VIDEOS_CAP; 263 + // Clamp src_quality 264 + if (p->src_quality < 0 || p->src_quality > SRC_Q_AUTO) p->src_quality = SRC_Q_SOURCE; 265 + // Clamp dither_mode 266 + if (p->dither_mode < 0 || p->dither_mode > DITHER_MODE_GRAY) p->dither_mode = DITHER_MODE_FLOYD; 267 + // Clamp fps_log_interval — 0 = off, otherwise clamp to [1, 60] 268 + if (p->fps_log_interval < 0) p->fps_log_interval = 0; 269 + if (p->fps_log_interval > 60) p->fps_log_interval = 60; 270 + // Clamp multicore — MC_AUTO (-1), MC_OFF (0), or [2..mp_cnt] 271 + if (p->multicore < MC_AUTO) p->multicore = MC_AUTO; 272 + if (p->multicore > mp_cnt) p->multicore = mp_cnt; 273 + if (p->multicore == 1) p->multicore = MC_OFF; // 1 is meaningless 274 + 275 + // Push debug flag immediately so the gate in DrainDebug picks it up 276 + g_debug_msgs = p->debug_msgs; 277 + } 278 + 279 + // Build the core policy from a multicore mode. Called from BrowseInit 280 + // after prefs are loaded. Auto => net on core 1, dec on core 2 if >=3 281 + // cores available; Off => both on core 0 (co-scheduled with UI); N 282 + // cores => net on core 1, dec on core 1 (N=2) or core 2 (N>=3). 283 + U0 CorePolicyBuild(CCorePolicy *cp, I64 mode, I64 ncpus) 284 + { 285 + if (mode == MC_OFF || ncpus < 2) { 286 + cp->net_core = 0; 287 + cp->dec_core = 0; 288 + return; 289 + } 290 + I64 want = mode; 291 + if (mode == MC_AUTO) want = ncpus; 292 + if (want > ncpus) want = ncpus; 293 + if (want < 2) want = 2; 294 + cp->net_core = 1; 295 + cp->dec_core = 1; 296 + if (want >= 3 && ncpus >= 3) cp->dec_core = 2; 297 + }
+579
holyc/vodbrowser/Settings.HC
··· 1 + // vodbrowser/Settings.HC — grid-style Settings UI 2 + // 3 + // Replaces the old GetI64-driven typed menu with arrow-key + mouse 4 + // navigation in the same visual style as Menu.HC: a vertical list of 5 + // setting rows with a red highlight on the current selection, and a 6 + // footer with Save / Back buttons at the bottom. Both the keyboard 7 + // and mouse route through the same set_sel_idx state so hovering + 8 + // clicking a row has the same effect as moving the selection there 9 + // with arrow keys and pressing Enter. 10 + // 11 + // Row semantics: 12 + // - Enum rows (display res, source quality, dither, multicore, 13 + // debug msgs): Enter / click cycles to the next value in-place. 14 + // This is the whole point of the rewrite — you no longer have 15 + // to type a number to pick an option. 16 + // - Text rows (PDS, DID, VOD server): Enter / click pops a GetStr 17 + // prompt below the list. The URL case is inherently typed so 18 + // there's nothing to grid-ify. 19 + // - Int rows (video fetch count, fps log interval): Enter / click 20 + // pops a numeric prompt. fps_log_interval uses a custom path 21 + // because 0 is a valid value (disables the log) whereas EditI64 22 + // treats 0 as cancel. 23 + // 24 + // Depends on: 25 + // - vodbrowser/Util.HC (SafeStrCpy, Mouse* helpers) 26 + // - vodbrowser/Prefs.HC (CPrefs, SRC_Q_*, MC_*, DITHER_MODE_*, 27 + // MAX_VIDEOS_CAP, PREFS_PATH) 28 + // - vodbrowser/lib/Media.HC (MediaSetDitherMode — live push on Save) 29 + 30 + // -------------------------------------------------------------------- 31 + // Selection indices. Rows 0..SET_N_ROWS-1 are the settings themselves 32 + // (in the display order used by RenderSettings); the three footer 33 + // buttons (SET_SEL_CONTINUE, SET_SEL_SAVE_CONT, SET_SEL_ABANDON) sit 34 + // past the rows. set_sel_idx names whichever one is currently 35 + // highlighted. 36 + // -------------------------------------------------------------------- 37 + #define SET_ROW_DISP_RES 0 38 + #define SET_ROW_SRC_Q 1 39 + #define SET_ROW_DITHER 2 40 + #define SET_ROW_PDS 3 41 + #define SET_ROW_DID 4 42 + #define SET_ROW_VOD 5 43 + #define SET_ROW_FETCH_CNT 6 44 + #define SET_ROW_MULTICORE 7 45 + #define SET_ROW_DEBUG 8 46 + #define SET_ROW_FPS_LOG 9 47 + #define SET_ROW_SKIP_NON_REF 10 48 + #define SET_N_ROWS 11 49 + // Footer buttons. Three options instead of Save/Back so the user 50 + // can change settings for the current session even on read-only 51 + // media (live CD, mounted sources.iso) where PrefsSave can't write. 52 + // CONTINUE applies edits to g_prefs and Media.prg in memory and 53 + // exits the menu. SAVE_CONT does the same plus writes to disk. 54 + // ABANDON discards edits without touching g_prefs. 55 + #define SET_SEL_CONTINUE 11 56 + #define SET_SEL_SAVE_CONT 12 57 + #define SET_SEL_ABANDON 13 58 + #define SET_N_TOTAL 14 59 + 60 + I64 set_sel_idx; 61 + 62 + // -------------------------------------------------------------------- 63 + // Value stringifiers. These are kept separate from the render code so 64 + // we can reuse them inside any "show current value" context (not just 65 + // the menu). Each writes into a caller-supplied buffer that should be 66 + // at least 64 bytes. 67 + // -------------------------------------------------------------------- 68 + U0 DispResName(U8 *buf, I64 w, I64 h) 69 + { 70 + if (w <= 0 || h <= 0) { 71 + StrPrint(buf, "Native (source)"); 72 + return; 73 + } 74 + if (w == 240 && h == 135) { StrPrint(buf, "Tiny (240x135)"); return; } 75 + if (w == 320 && h == 180) { StrPrint(buf, "Small (320x180)"); return; } 76 + if (w == 400 && h == 225) { StrPrint(buf, "Medium (400x225)"); return; } 77 + if (w == 560 && h == 315) { StrPrint(buf, "Large (560x315)"); return; } 78 + if (w == 640 && h == 360) { StrPrint(buf, "Max (640x360)"); return; } 79 + StrPrint(buf, "Custom (%dx%d)", w, h); 80 + } 81 + 82 + U0 SrcQualityName(U8 *buf, I64 q) 83 + { 84 + if (q == SRC_Q_SOURCE) { StrPrint(buf, "Source (server order)"); return; } 85 + if (q == SRC_Q_HIGHEST) { StrPrint(buf, "Highest (most pixels)"); return; } 86 + if (q == SRC_Q_LOWEST) { StrPrint(buf, "Lowest (fewest pixels)"); return; } 87 + if (q == SRC_Q_AUTO) { StrPrint(buf, "Auto (fit display)"); return; } 88 + StrPrint(buf, "?"); 89 + } 90 + 91 + U0 MulticoreName(U8 *buf, I64 mc) 92 + { 93 + if (mc == MC_OFF) { StrPrint(buf, "Off (single core)"); return; } 94 + if (mc == MC_AUTO) { StrPrint(buf, "Auto (%d cores)", mp_cnt); return; } 95 + StrPrint(buf, "Explicit (N=%d)", mc); 96 + } 97 + 98 + U0 DitherModeName(U8 *buf, I64 m) 99 + { 100 + if (m == DITHER_MODE_FLOYD) { StrPrint(buf, "Floyd-Steinberg (quality)"); return; } 101 + if (m == DITHER_MODE_NEAREST) { StrPrint(buf, "Nearest (simple)"); return; } 102 + if (m == DITHER_MODE_GRAY) { StrPrint(buf, "Grayscale (4 grays)"); return; } 103 + StrPrint(buf, "?"); 104 + } 105 + 106 + // Shorten a long string so it fits in a fixed-width cell. Anything 107 + // longer than `max` chars is chopped and tail-replaced with "..." so 108 + // the name/value columns in the settings list stay aligned when the 109 + // user points a long URL at their PDS. 110 + U0 SetTruncate(U8 *dst, U8 *src, I64 max) 111 + { 112 + if (!dst || max <= 0) return; 113 + if (!src) { dst[0] = 0; return; } 114 + I64 len = StrLen(src); 115 + if (len <= max) { 116 + SafeStrCpy(dst, src, max + 1); 117 + return; 118 + } 119 + I64 i; 120 + for (i = 0; i < max - 3; i++) dst[i] = src[i]; 121 + dst[max - 3] = '.'; 122 + dst[max - 2] = '.'; 123 + dst[max - 1] = '.'; 124 + dst[max] = 0; 125 + } 126 + 127 + // -------------------------------------------------------------------- 128 + // Sub-prompts for text/int rows. These drop out of the grid UI briefly 129 + // to read a typed value, then return so the outer loop re-renders. 130 + // -------------------------------------------------------------------- 131 + 132 + // Prompt for a single-line string replacement. Shows the current 133 + // value, reads a new value via GetStr, copies into `dst` if non-empty. 134 + // Returns TRUE if the value actually changed; empty-Enter or an 135 + // unchanged string both return FALSE. 136 + Bool EditString(U8 *label, U8 *dst, I64 max) 137 + { 138 + "\n %s (Enter to cancel)\n", label; 139 + " Current: %s\n", dst; 140 + " New : "; 141 + Bool changed = FALSE; 142 + U8 *line = GetStr; 143 + if (line) { 144 + if (line[0] && StrCmp(line, dst)) { 145 + SafeStrCpy(dst, line, max); 146 + changed = TRUE; 147 + } 148 + Free(line); 149 + } 150 + return changed; 151 + } 152 + 153 + // Prompt for an integer in [lo, hi]. Values outside the range are 154 + // clamped. An empty Enter (GetI64 returns 0) is treated as cancel — 155 + // so this helper can't be used for settings where 0 is a valid 156 + // value. Returns TRUE if *dst was updated. 157 + Bool EditI64(U8 *label, I64 *dst, I64 lo, I64 hi) 158 + { 159 + "\n %s (%d..%d, Enter to cancel)\n", label, lo, hi; 160 + " Current: %d\n", *dst; 161 + " New : "; 162 + I64 v = GetI64; 163 + if (v == 0) return FALSE; 164 + if (v < lo) v = lo; 165 + if (v > hi) v = hi; 166 + if (v == *dst) return FALSE; 167 + *dst = v; 168 + return TRUE; 169 + } 170 + 171 + // -------------------------------------------------------------------- 172 + // Enum cycling — called on Enter / click / Left / Right for any row 173 + // that has a fixed, small set of valid values. For rows that are not 174 + // cycleable (text/int), this is a no-op returning FALSE so the caller 175 + // can fall through to the prompt path. 176 + // -------------------------------------------------------------------- 177 + Bool SetCycleEnum(I64 row) 178 + { 179 + if (row == SET_ROW_DISP_RES) { 180 + I64 w = g_prefs_edit.disp_w; 181 + if (w == 240) { g_prefs_edit.disp_w = 320; g_prefs_edit.disp_h = 180; } 182 + else if (w == 320) { g_prefs_edit.disp_w = 400; g_prefs_edit.disp_h = 225; } 183 + else if (w == 400) { g_prefs_edit.disp_w = 560; g_prefs_edit.disp_h = 315; } 184 + else if (w == 560) { g_prefs_edit.disp_w = 640; g_prefs_edit.disp_h = 360; } 185 + else if (w == 640) { g_prefs_edit.disp_w = 0; g_prefs_edit.disp_h = 0; } 186 + else { g_prefs_edit.disp_w = 240; g_prefs_edit.disp_h = 135; } 187 + return TRUE; 188 + } 189 + if (row == SET_ROW_SRC_Q) { 190 + g_prefs_edit.src_quality++; 191 + if (g_prefs_edit.src_quality > SRC_Q_AUTO) 192 + g_prefs_edit.src_quality = SRC_Q_SOURCE; 193 + return TRUE; 194 + } 195 + if (row == SET_ROW_DITHER) { 196 + g_prefs_edit.dither_mode++; 197 + if (g_prefs_edit.dither_mode > DITHER_MODE_GRAY) 198 + g_prefs_edit.dither_mode = DITHER_MODE_FLOYD; 199 + return TRUE; 200 + } 201 + if (row == SET_ROW_MULTICORE) { 202 + // MC_OFF → MC_AUTO → N=2 → N=3 → ... → N=mp_cnt → MC_OFF 203 + if (g_prefs_edit.multicore == MC_OFF) { 204 + g_prefs_edit.multicore = MC_AUTO; 205 + } else if (g_prefs_edit.multicore == MC_AUTO) { 206 + if (mp_cnt >= 2) g_prefs_edit.multicore = 2; 207 + else g_prefs_edit.multicore = MC_OFF; 208 + } else if (g_prefs_edit.multicore < mp_cnt) { 209 + g_prefs_edit.multicore++; 210 + } else { 211 + g_prefs_edit.multicore = MC_OFF; 212 + } 213 + return TRUE; 214 + } 215 + if (row == SET_ROW_DEBUG) { 216 + g_prefs_edit.debug_msgs = !g_prefs_edit.debug_msgs; 217 + return TRUE; 218 + } 219 + if (row == SET_ROW_SKIP_NON_REF) { 220 + g_prefs_edit.skip_non_ref = !g_prefs_edit.skip_non_ref; 221 + return TRUE; 222 + } 223 + return FALSE; 224 + } 225 + 226 + // Activate the given row — Enter key or mouse click. Dispatches 227 + // enum rows to SetCycleEnum and text/int rows to their prompt 228 + // helpers. Marks g_prefs_dirty on any actual change so the header 229 + // shows "(unsaved)" and the Save button becomes meaningful. 230 + U0 SetActivate(I64 row) 231 + { 232 + Bool changed = FALSE; 233 + if (row == SET_ROW_DISP_RES || 234 + row == SET_ROW_SRC_Q || 235 + row == SET_ROW_DITHER || 236 + row == SET_ROW_MULTICORE || 237 + row == SET_ROW_DEBUG || 238 + row == SET_ROW_SKIP_NON_REF) { 239 + changed = SetCycleEnum(row); 240 + } else if (row == SET_ROW_PDS) { 241 + changed = EditString("PDS (host URL)", g_prefs_edit.pds, 256); 242 + } else if (row == SET_ROW_DID) { 243 + changed = EditString("DID (account id)", g_prefs_edit.did, 160); 244 + } else if (row == SET_ROW_VOD) { 245 + changed = EditString("VOD server base URL", g_prefs_edit.vod_base, 256); 246 + } else if (row == SET_ROW_FETCH_CNT) { 247 + changed = EditI64("Video fetch count", 248 + &g_prefs_edit.video_fetch_count, 1, MAX_VIDEOS_CAP); 249 + } else if (row == SET_ROW_FPS_LOG) { 250 + // Custom prompt because 0 is valid here (disables logging) and 251 + // EditI64 treats 0 as cancel. GetStr lets us tell "" (cancel) 252 + // apart from "0" (disable). 253 + "\n FPS log interval (0 = off, 1..60 sec, Enter to cancel)\n"; 254 + " Current: %d\n", g_prefs_edit.fps_log_interval; 255 + " New : "; 256 + U8 *line = GetStr; 257 + if (line) { 258 + if (line[0]) { 259 + I64 v = Str2I64(line); 260 + if (v < 0) v = 0; 261 + if (v > 60) v = 60; 262 + if (v != g_prefs_edit.fps_log_interval) { 263 + g_prefs_edit.fps_log_interval = v; 264 + changed = TRUE; 265 + } 266 + } 267 + Free(line); 268 + } 269 + } 270 + if (changed) g_prefs_dirty = TRUE; 271 + } 272 + 273 + // -------------------------------------------------------------------- 274 + // Render — one row per setting with a red highlight on the currently 275 + // selected row, then a "Save / Back" footer styled like Menu.HC. 276 + // -------------------------------------------------------------------- 277 + U0 SetRenderRow(I64 row_idx, U8 *name, U8 *value) 278 + { 279 + U8 *col_open = ""; 280 + U8 *col_close = ""; 281 + if (set_sel_idx == row_idx) { 282 + col_open = "$$FG,LTRED$$"; 283 + col_close = "$$FG$$"; 284 + } 285 + // Hand-align the name column to 20 chars — HolyC's StrPrint 286 + // doesn't reliably support "%-20s"-style width modifiers so we 287 + // pad with explicit spaces. 20 chars is enough for the longest 288 + // label below ("Display resolution", 18). 289 + U8 padded[24]; 290 + I64 i = 0; 291 + while (name[i] && i < 20) { padded[i] = name[i]; i++; } 292 + while (i < 20) { padded[i] = ' '; i++; } 293 + padded[20] = 0; 294 + " %s%s: %s%s\n", col_open, padded, value, col_close; 295 + } 296 + 297 + U0 SetRenderBtn(I64 btn_idx, U8 *label) 298 + { 299 + U8 *col_open = ""; 300 + U8 *col_close = ""; 301 + if (set_sel_idx == btn_idx) { 302 + col_open = "$$FG,LTRED$$"; 303 + col_close = "$$FG$$"; 304 + } 305 + "%s%s%s", col_open, label, col_close; 306 + } 307 + 308 + U0 RenderSettings() 309 + { 310 + // Reset the hit region table each frame and rebuild in lockstep 311 + // with the print statements below. Click-to-select is therefore 312 + // always in sync with whatever is actually on screen — if rows 313 + // shift around because we add a new setting, the hit test moves 314 + // with them automatically. 315 + HitReset; 316 + DocClear; 317 + 318 + U8 disp_name[64], src_name[64], mc_name[64], dith_name[64]; 319 + DispResName (disp_name, g_prefs_edit.disp_w, g_prefs_edit.disp_h); 320 + SrcQualityName(src_name, g_prefs_edit.src_quality); 321 + MulticoreName (mc_name, g_prefs_edit.multicore); 322 + DitherModeName(dith_name, g_prefs_edit.dither_mode); 323 + 324 + U8 pds_disp[48], did_disp[48], vod_disp[48]; 325 + SetTruncate(pds_disp, g_prefs_edit.pds, 44); 326 + SetTruncate(did_disp, g_prefs_edit.did, 44); 327 + SetTruncate(vod_disp, g_prefs_edit.vod_base, 44); 328 + 329 + U8 fetch_buf[32], fps_buf[32], dbg_buf[16], skip_buf[16]; 330 + StrPrint(fetch_buf, "%d", g_prefs_edit.video_fetch_count); 331 + if (g_prefs_edit.fps_log_interval == 0) StrPrint(fps_buf, "Off"); 332 + else StrPrint(fps_buf, "%d sec", g_prefs_edit.fps_log_interval); 333 + if (g_prefs_edit.debug_msgs) StrPrint(dbg_buf, "On"); 334 + else StrPrint(dbg_buf, "Off"); 335 + if (g_prefs_edit.skip_non_ref) StrPrint(skip_buf, "On"); 336 + else StrPrint(skip_buf, "Off"); 337 + 338 + U8 *dirty_tag = ""; 339 + if (g_prefs_dirty) dirty_tag = " (unsaved)"; 340 + 341 + // Header — matches Menu.HC styling so the two screens feel cohesive. 342 + // BLUE on white gives the same good contrast the Browse title has. 343 + "\n$$FG,BLUE$$$$TX+CX,\"VodBrowser - Settings%s\"$$$$FG$$\n\n", dirty_tag; 344 + g_render_row = 3; 345 + 346 + // Each setting row consumes exactly one doc row. SetRenderRow 347 + // prints the label/value and we immediately record the hit 348 + // region for it and bump g_render_row. The click column range 349 + // is deliberately generous (cols 2..70) so clicking anywhere on 350 + // the row — label, colon, or value — selects it. 351 + SetRenderRow(SET_ROW_DISP_RES, "Display resolution", disp_name); 352 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_DISP_RES); 353 + g_render_row++; 354 + 355 + SetRenderRow(SET_ROW_SRC_Q, "Source resolution", src_name); 356 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_SRC_Q); 357 + g_render_row++; 358 + 359 + SetRenderRow(SET_ROW_DITHER, "Dither mode", dith_name); 360 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_DITHER); 361 + g_render_row++; 362 + 363 + SetRenderRow(SET_ROW_PDS, "PDS (host URL)", pds_disp); 364 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_PDS); 365 + g_render_row++; 366 + 367 + SetRenderRow(SET_ROW_DID, "DID (account id)", did_disp); 368 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_DID); 369 + g_render_row++; 370 + 371 + SetRenderRow(SET_ROW_VOD, "VOD server", vod_disp); 372 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_VOD); 373 + g_render_row++; 374 + 375 + SetRenderRow(SET_ROW_FETCH_CNT, "Video fetch count", fetch_buf); 376 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_FETCH_CNT); 377 + g_render_row++; 378 + 379 + SetRenderRow(SET_ROW_MULTICORE, "Multicore", mc_name); 380 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_MULTICORE); 381 + g_render_row++; 382 + 383 + SetRenderRow(SET_ROW_DEBUG, "Debug messages", dbg_buf); 384 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_DEBUG); 385 + g_render_row++; 386 + 387 + SetRenderRow(SET_ROW_FPS_LOG, "FPS log interval", fps_buf); 388 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_FPS_LOG); 389 + g_render_row++; 390 + 391 + SetRenderRow(SET_ROW_SKIP_NON_REF, "Skip non-ref frames", skip_buf); 392 + HitAdd(2, 70, g_render_row, g_render_row + 1, SET_ROW_SKIP_NON_REF); 393 + g_render_row++; 394 + 395 + "\n$$FG,DKGRAY$$ (Arrows move . Click or Enter edits . Esc = Abandon)$$FG$$\n"; 396 + g_render_row += 2; 397 + 398 + // Footer buttons — Continue, Save & Continue, Abandon. Each one 399 + // is rendered as a bracket-wrapped label and paired with a hit 400 + // region that covers exactly the label's character range so 401 + // clicking in the gap between buttons doesn't accidentally fire. 402 + // 403 + // [ Continue ] 12 chars 404 + // [ Save & Continue ] 19 chars 405 + // [ Abandon ] 11 chars 406 + // 407 + // Separated by " " (2 chars) for breathing room. 408 + "\n "; 409 + g_render_row++; 410 + I64 footer_row = g_render_row; 411 + I64 cx = 2; 412 + 413 + SetRenderBtn(SET_SEL_CONTINUE, "[ Continue ]"); 414 + HitAdd(cx, cx + 12, footer_row, footer_row + 1, SET_SEL_CONTINUE); 415 + " "; 416 + cx += 14; 417 + 418 + SetRenderBtn(SET_SEL_SAVE_CONT, "[ Save & Continue ]"); 419 + HitAdd(cx, cx + 19, footer_row, footer_row + 1, SET_SEL_SAVE_CONT); 420 + " "; 421 + cx += 21; 422 + 423 + SetRenderBtn(SET_SEL_ABANDON, "[ Abandon ]"); 424 + HitAdd(cx, cx + 11, footer_row, footer_row + 1, SET_SEL_ABANDON); 425 + "\n"; 426 + g_render_row++; 427 + } 428 + 429 + // -------------------------------------------------------------------- 430 + // Main entry — owns the fullscreen context and the input loop. 431 + // -------------------------------------------------------------------- 432 + U0 ShowSettings() 433 + { 434 + // Working copy: edits live here until Save. This lets the user 435 + // back out with Esc and lose nothing. 436 + MemCpy(&g_prefs_edit, &g_prefs, sizeof(CPrefs)); 437 + g_prefs_dirty = FALSE; 438 + set_sel_idx = 0; 439 + 440 + // Push a fresh fullscreen context so the layout math (used by 441 + // SetHitTest) lines up with what's on screen. SettingsPop restores 442 + // the caller's border/size/cursor on exit — this nests cleanly 443 + // under VodBrowser's own SettingsPush/Pop. 444 + SettingsPush; 445 + WinBorder; 446 + WinMax; 447 + DocCursor; 448 + 449 + I64 ch, sc; 450 + I64 r; 451 + Bool done = FALSE; 452 + while (!done) { 453 + RenderSettings; 454 + 455 + // Wait for a key or a mouse click. Click sets set_sel_idx to the 456 + // hit target and synthesises an Enter on the downstream handler. 457 + // A click on empty space is consumed (g_prev_mouse_lb updates) 458 + // but ignored — the selection only moves on real hits. 459 + Bool click_activate = FALSE; 460 + while (TRUE) { 461 + if (ScanKey(&ch, &sc)) break; 462 + Bool click = MouseClickEdge; 463 + if (click) { 464 + I64 hit = HitFind(MouseTextCol, MouseTextRow); 465 + if (hit >= 0) { 466 + set_sel_idx = hit; 467 + click_activate = TRUE; 468 + break; 469 + } 470 + } 471 + Sleep(20); 472 + } 473 + FlushMsgs; 474 + 475 + if (click_activate) { 476 + ch = '\n'; 477 + sc = 0; 478 + } 479 + 480 + I64 k = sc & 0xFF; 481 + 482 + if (ch == CH_ESC) { 483 + // Esc is a shortcut for Abandon: discard edits without 484 + // touching g_prefs. Confirm if there are unsaved changes so 485 + // an accidental keypress doesn't throw work away. 486 + Bool really_abandon = TRUE; 487 + if (g_prefs_dirty) { 488 + "\n Abandon unsaved changes? 1=yes, 0=no: "; 489 + r = GetI64; 490 + if (r != 1) really_abandon = FALSE; 491 + } 492 + if (really_abandon) done = TRUE; 493 + } else if (ch == '\n') { 494 + if (set_sel_idx == SET_SEL_CONTINUE) { 495 + // Apply edits in memory and exit the menu. No disk write, 496 + // so this works on read-only media (live CD, mounted 497 + // sources.iso). DecodeTask is idle between playbacks 498 + // (we're in the menu), so the thunk globals are 499 + // uncontested for the Media.prg pushes. 500 + if (g_prefs_dirty) { 501 + MemCpy(&g_prefs, &g_prefs_edit, sizeof(CPrefs)); 502 + PrefsApply(&g_prefs); 503 + MediaSetDitherMode(g_prefs.dither_mode); 504 + MediaSetSkipNonRef(g_prefs.skip_non_ref); 505 + g_prefs_dirty = FALSE; 506 + } 507 + done = TRUE; 508 + } else if (set_sel_idx == SET_SEL_SAVE_CONT) { 509 + // Apply edits in memory AND write to disk. PrefsSave 510 + // returns <0 on write failure (read-only media); in that 511 + // case we still apply in memory and continue — the whole 512 + // point of this layout is to never get stuck because the 513 + // drive can't be written. 514 + if (g_prefs_dirty) { 515 + MemCpy(&g_prefs, &g_prefs_edit, sizeof(CPrefs)); 516 + PrefsApply(&g_prefs); 517 + MediaSetDitherMode(g_prefs.dither_mode); 518 + MediaSetSkipNonRef(g_prefs.skip_non_ref); 519 + I64 save_rc = PrefsSave(&g_prefs, PREFS_PATH); 520 + if (save_rc >= 0) { 521 + "\n Saved to %s\n", PREFS_PATH; 522 + } else { 523 + "\n Save failed (read-only media?); applied in memory only.\n"; 524 + } 525 + g_prefs_dirty = FALSE; 526 + Sleep(400); 527 + } 528 + done = TRUE; 529 + } else if (set_sel_idx == SET_SEL_ABANDON) { 530 + // Discard edits. Confirm only if there are any. 531 + Bool really_abandon2 = TRUE; 532 + if (g_prefs_dirty) { 533 + "\n Abandon unsaved changes? 1=yes, 0=no: "; 534 + r = GetI64; 535 + if (r != 1) really_abandon2 = FALSE; 536 + } 537 + if (really_abandon2) done = TRUE; 538 + } else if (set_sel_idx >= 0 && set_sel_idx < SET_N_ROWS) { 539 + SetActivate(set_sel_idx); 540 + } 541 + } else if (ch == 0) { 542 + // Arrow navigation. 543 + if (k == SC_CURSOR_UP) { 544 + if (set_sel_idx > 0) set_sel_idx--; 545 + } 546 + if (k == SC_CURSOR_DOWN) { 547 + if (set_sel_idx < SET_N_TOTAL - 1) set_sel_idx++; 548 + } 549 + if (k == SC_CURSOR_LEFT) { 550 + // In the footer row, Left walks backward through the three 551 + // buttons: Abandon → Save & Continue → Continue → stays. 552 + // In the row area Left cycles the enum forward (there's 553 + // no reverse-cycle helper and every enum here is small 554 + // enough that one extra step around the loop is cheap). 555 + // Rows that aren't cycleable ignore the keypress via 556 + // SetCycleEnum returning FALSE. 557 + if (set_sel_idx == SET_SEL_ABANDON) { 558 + set_sel_idx = SET_SEL_SAVE_CONT; 559 + } else if (set_sel_idx == SET_SEL_SAVE_CONT) { 560 + set_sel_idx = SET_SEL_CONTINUE; 561 + } else if (set_sel_idx < SET_N_ROWS) { 562 + if (SetCycleEnum(set_sel_idx)) g_prefs_dirty = TRUE; 563 + } 564 + } 565 + if (k == SC_CURSOR_RIGHT) { 566 + // Symmetric: Continue → Save & Continue → Abandon → stays. 567 + if (set_sel_idx == SET_SEL_CONTINUE) { 568 + set_sel_idx = SET_SEL_SAVE_CONT; 569 + } else if (set_sel_idx == SET_SEL_SAVE_CONT) { 570 + set_sel_idx = SET_SEL_ABANDON; 571 + } else if (set_sel_idx < SET_N_ROWS) { 572 + if (SetCycleEnum(set_sel_idx)) g_prefs_dirty = TRUE; 573 + } 574 + } 575 + } 576 + } 577 + 578 + SettingsPop; 579 + }
+297
holyc/vodbrowser/Util.HC
··· 1 + // vodbrowser/Util.HC — small helpers and debug channels 2 + // 3 + // This file is the foundation layer for the VodBrowser application: 4 + // - Worker → main debug message channels (DrainDebug) 5 + // - String helpers (SafeStrCpy, UrlEncode) 6 + // - Async-fetch wrappers over Net.HC's NetFetchBegin/Tick/Result 7 + // - Byte-buffer / HLS-playlist parsing utilities 8 + // 9 + // Must be #included AFTER vodbrowser/lib/Net.HC because AsyncFetch and 10 + // friends call NetFetchBegin etc. from that file. 11 + 12 + // ================================================================== 13 + // Inter-task debug channel — workers write short status strings, 14 + // main task drains them to the console. 15 + // ================================================================== 16 + 17 + U8 net_dbg[256]; 18 + I64 net_dbg_ready; 19 + U8 dec_dbg[256]; 20 + I64 dec_dbg_ready; 21 + 22 + // Forward declaration of the debug-messages pref (defined in CPrefs 23 + // below). DrainDebug is called from hot paths so we want the gate as 24 + // cheap as possible — a single global load. 25 + I64 g_debug_msgs; 26 + 27 + U0 DrainDebug() 28 + { 29 + if (!g_debug_msgs) { 30 + // Swallow the messages so workers don't keep "ready" flags set 31 + // forever, but don't print them. 32 + net_dbg_ready = FALSE; 33 + dec_dbg_ready = FALSE; 34 + return; 35 + } 36 + if (net_dbg_ready) { "%s\n", net_dbg; net_dbg_ready = FALSE; } 37 + if (dec_dbg_ready) { "%s\n", dec_dbg; dec_dbg_ready = FALSE; } 38 + } 39 + 40 + // ================================================================== 41 + // String utilities 42 + // ================================================================== 43 + 44 + // Bounded, null-terminating string copy. Unlike StrCpy it never 45 + // overruns dst, and it always null-terminates (even when src is empty). 46 + U0 SafeStrCpy(U8 *dst, U8 *src, I64 max) 47 + { 48 + if (!dst || max <= 0) return; 49 + if (!src) { dst[0] = 0; return; } 50 + I64 n = 0; 51 + while (src[n] && n < max - 1) { dst[n] = src[n]; n++; } 52 + dst[n] = 0; 53 + } 54 + 55 + U0 UrlEncode(U8 *dst, U8 *src, I64 max) 56 + { 57 + U8 *hex = "0123456789ABCDEF"; 58 + I64 di = 0, si = 0; 59 + while (src[si] && di + 3 < max) { 60 + I64 c = src[si]; 61 + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || 62 + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { 63 + dst[di++] = c; 64 + } else { 65 + dst[di++] = '%'; 66 + dst[di++] = hex[c >> 4]; 67 + dst[di++] = hex[c & 0x0F]; 68 + } 69 + si++; 70 + } 71 + dst[di] = 0; 72 + } 73 + 74 + // ================================================================== 75 + // Async-fetch wrappers (blocking on the NetTask worker) 76 + // ================================================================== 77 + 78 + // Drive the async fetch state machine to completion. Runs in NetTask. 79 + I64 AsyncFetch(U8 *url, I64 *out_status, U8 **out_body, I64 *out_len) 80 + { 81 + I64 rc = NetFetchBegin(url); 82 + if (rc < 0) return rc; 83 + I64 ft; 84 + while (TRUE) { 85 + ft = NetFetchTick; 86 + if (ft != NET_FETCH_PENDING) break; 87 + Sleep(10); 88 + } 89 + // Always call NetFetchResult — on NET_FETCH_ERROR it returns the 90 + // C-side fctx.error_code (negative, typically in the -10..-700 range) 91 + // which is far more actionable than a generic "something failed" 92 + // sentinel. Old code returned -999 here which lost the real reason. 93 + U8 *rsp = CAlloc(24); 94 + rc = NetFetchResult(rsp); 95 + if (rc == 0) { 96 + U64 *rp = rsp; 97 + *out_status = rp[0]; 98 + *out_body = rp[1]; 99 + *out_len = rp[2]; 100 + } 101 + Free(rsp); 102 + return rc; 103 + } 104 + 105 + I64 AsyncFetchRange(U8 *url, I64 start, I64 end, 106 + I64 *out_status, U8 **out_body, I64 *out_len) 107 + { 108 + U8 hdr[128]; 109 + StrPrint(hdr, "Range: bytes=%d-%d\r\n", start, end); 110 + NetSetHeaders(hdr); 111 + return AsyncFetch(url, out_status, out_body, out_len); 112 + } 113 + 114 + // ================================================================== 115 + // Byte-buffer scanning / HLS playlist parsing 116 + // ================================================================== 117 + 118 + U8 *BufFind(U8 *buf, I64 buf_len, U8 *needle) 119 + { 120 + I64 nlen = StrLen(needle); 121 + I64 i; 122 + for (i = 0; i + nlen <= buf_len; i++) { 123 + I64 j; 124 + I64 match = TRUE; 125 + for (j = 0; j < nlen; j++) { 126 + if (buf[i + j] != needle[j]) { match = FALSE; break; } 127 + } 128 + if (match) return buf + i; 129 + } 130 + return NULL; 131 + } 132 + 133 + I64 CopyLine(U8 *dst, U8 *src, U8 *end, I64 max) 134 + { 135 + I64 i = 0; 136 + while (src + i < end && src[i] != '\r' && src[i] != '\n' && i < max - 1) { 137 + dst[i] = src[i]; 138 + i++; 139 + } 140 + dst[i] = 0; 141 + return i; 142 + } 143 + 144 + U8 *HlsNextLine(U8 *p, U8 *end) 145 + { 146 + while (p < end && *p != '\n') p++; 147 + if (p < end) p++; 148 + return p; 149 + } 150 + 151 + // ================================================================== 152 + // Mouse input helpers — used by Menu.HC and Settings.HC for click 153 + // support over the Browse and Settings UIs. 154 + // ================================================================== 155 + 156 + // TempleOS mouse state lives in the global CMsStateGlbls `ms`, with 157 + // `ms.pos.x/y` in SCREEN pixels (not task-window pixels). To turn 158 + // those into the doc-local text cells our render code uses, we have 159 + // to: 160 + // 161 + // 1. Convert pixels to character cells by dividing by the 8-px 162 + // default font width/height. 163 + // 2. Subtract the task window's own origin — Fs->win_left and 164 + // Fs->win_top are in character cells and mark where the top- 165 + // left corner of the task window sits on screen. TempleOS 166 + // draws a task band / taskbar above the window by default 167 + // even with WinBorder off, so win_top is usually 1 and 168 + // win_left is 0. 169 + // 170 + // Without the (2) offset, clicking on the first visible row of 171 + // content inside the window actually reports one row too high and 172 + // the hit test mis-fires by a full row — which is exactly the 173 + // symptom we chased on the first click-support deploy. 174 + I64 MouseTextCol() { return (ms.pos.x >> 3) - Fs->win_left; } 175 + I64 MouseTextRow() { return (ms.pos.y >> 3) - Fs->win_top; } 176 + 177 + // Rising-edge detector for the left mouse button. Each call returns 178 + // TRUE exactly once per press — polling it in the input loop gives 179 + // "single click" semantics. Uses a file-scope global because HolyC 180 + // doesn't consistently support function-local statics across the 181 + // single-pass compiler's memory lifetime for ShowSettings and Browse 182 + // loops that share this helper. 183 + I64 g_prev_mouse_lb; 184 + 185 + Bool MouseClickEdge() 186 + { 187 + Bool now = FALSE; 188 + if (ms.lb) now = TRUE; 189 + Bool edge = now && !g_prev_mouse_lb; 190 + g_prev_mouse_lb = now; 191 + return edge; 192 + } 193 + 194 + // ================================================================== 195 + // Click hit region table — populated by render code, queried by the 196 + // input loop. Replaces the previous hardcoded row/col bands in 197 + // Menu.HC / Settings.HC. The render function walks its layout once, 198 + // emits text into the DolDoc *and* records which doc region each 199 + // printed thing occupies, then the input loop calls HitFind(col, row) 200 + // to resolve a mouse click back to a selection index. 201 + // 202 + // Keeping the regions in a flat table means any change to the 203 + // render layout automatically updates the hit test — we don't have 204 + // to track two copies of the geometry and keep them in sync. 205 + // ================================================================== 206 + 207 + class CHit { 208 + I64 col0; // inclusive 209 + I64 col1; // exclusive 210 + I64 row0; // inclusive 211 + I64 row1; // exclusive 212 + I64 target; // selection index this region resolves to 213 + }; 214 + 215 + // 32 slots is plenty for both screens (Browse has 6 tiles + 4 footer 216 + // buttons = 10, Settings has 10 rows + 2 buttons = 12). Bump if you 217 + // add another major layout. 218 + #define HIT_MAX 32 219 + CHit g_hits[HIT_MAX]; 220 + I64 g_hits_n; 221 + 222 + // Shared render-row counter. Render code increments this in lockstep 223 + // with the `\n`s it emits so HitAdd callers know exactly which doc 224 + // row they're on without counting characters. RenderMenuPage / 225 + // RenderSettings both call HitReset at the top to start fresh. 226 + I64 g_render_row; 227 + 228 + U0 HitReset() 229 + { 230 + g_hits_n = 0; 231 + g_render_row = 0; 232 + } 233 + 234 + U0 HitAdd(I64 col0, I64 col1, I64 row0, I64 row1, I64 target) 235 + { 236 + if (g_hits_n >= HIT_MAX) return; 237 + g_hits[g_hits_n].col0 = col0; 238 + g_hits[g_hits_n].col1 = col1; 239 + g_hits[g_hits_n].row0 = row0; 240 + g_hits[g_hits_n].row1 = row1; 241 + g_hits[g_hits_n].target = target; 242 + g_hits_n++; 243 + } 244 + 245 + // Linear scan — N is at most HIT_MAX (32) and we only run this on 246 + // actual clicks, so a loop is fine. Returns the first matching 247 + // region's target, or -1 if the click lands outside every region. 248 + I64 HitFind(I64 col, I64 row) 249 + { 250 + I64 i; 251 + for (i = 0; i < g_hits_n; i++) { 252 + if (col >= g_hits[i].col0 && col < g_hits[i].col1 && 253 + row >= g_hits[i].row0 && row < g_hits[i].row1) 254 + return g_hits[i].target; 255 + } 256 + return -1; 257 + } 258 + 259 + I64 ParseNum(U8 *s, U8 *end) 260 + { 261 + I64 val = 0; 262 + while (s < end && *s >= '0' && *s <= '9') { 263 + val = val * 10 + (*s - '0'); 264 + s++; 265 + } 266 + return val; 267 + } 268 + 269 + // Extract a KEY=value attribute from a bounded line (e.g. the attribute 270 + // portion of a #EXT-X-STREAM-INF: line). `key` must include the trailing 271 + // '=' (e.g. "RESOLUTION="). Only matches when the character before the 272 + // key is a boundary (':' or ','), avoiding substring false-positives 273 + // like matching "BANDWIDTH=" inside "AVERAGE-BANDWIDTH=". The value is 274 + // copied into `out` (null-terminated), stopping at ',' or end-of-line. 275 + Bool BufFindAttr(U8 *start, U8 *end, U8 *key, U8 *out, I64 max) 276 + { 277 + I64 klen = StrLen(key); 278 + U8 *p = start; 279 + while (p + klen <= end) { 280 + Bool boundary = (p == start || p[-1] == ':' || p[-1] == ','); 281 + I64 j; 282 + Bool match = boundary; 283 + for (j = 0; match && j < klen; j++) if (p[j] != key[j]) match = FALSE; 284 + if (match) { 285 + p += klen; 286 + I64 n = 0; 287 + while (p < end && n < max - 1 && *p != ',' && *p != '\n' && *p != '\r') { 288 + out[n++] = *p++; 289 + } 290 + out[n] = 0; 291 + return TRUE; 292 + } 293 + p++; 294 + } 295 + out[0] = 0; 296 + return FALSE; 297 + }
+174
holyc/vodbrowser/VideoList.HC
··· 1 + // vodbrowser/VideoList.HC — the browsable video list 2 + // 3 + // State: fixed-size arrays of titles + URIs populated by FetchVideoBatch 4 + // via ATProto's listRecords XRPC endpoint. `vid_next_cursor` carries 5 + // the server pagination cursor so each FetchVideoBatch picks up where 6 + // the previous one left off; `vid_exhausted` flips true once the server 7 + // signals end-of-list (no cursor returned or short batch). 8 + // 9 + // BFetch is the simple main-task → NetTask bridge used for ordinary 10 + // HTTP GETs (not HLS streaming). It posts a NET_MSG_FETCH message and 11 + // blocks until the worker writes the response globals. 12 + // 13 + // Depends on: vodbrowser/Util.HC (SafeStrCpy, UrlEncode), 14 + // vodbrowser/Prefs.HC (g_prefs), vodbrowser/NetTask.HC (net_msg queue). 15 + 16 + // Synchronous HTTP fetch via NetTask message queue. 17 + // Only safe to call when NetTask is idle (no pending NET_MSG_*). 18 + I64 BFetch(U8 *url, I64 *out_status, U8 **out_body, I64 *out_len) 19 + { 20 + net_req_url = url; 21 + net_resp_done = FALSE; 22 + net_msg = NET_MSG_FETCH; 23 + while (!net_resp_done) Sleep(100); 24 + if (net_resp_rc == 0) { 25 + *out_status = net_resp_status; 26 + *out_body = net_resp_body; 27 + *out_len = net_resp_body_len; 28 + } 29 + return net_resp_rc; 30 + } 31 + 32 + // The listing arrays are sized to the hard cap. `vid_count` grows 33 + // incrementally as the user pages forward — we fetch one batch of 34 + // g_prefs.video_fetch_count items at startup and another batch each 35 + // time the user hits Next past the loaded end. 36 + // 37 + // `vid_next_cursor` is the ATProto pagination cursor to use for the 38 + // next batch (empty string means "start from the beginning"). 39 + // `vid_exhausted` is set once the server returns a short batch or 40 + // no cursor — from that point on, the Next button is disabled. 41 + U8 *vid_titles[MAX_VIDEOS_CAP]; 42 + U8 *vid_uris[MAX_VIDEOS_CAP]; 43 + I64 vid_count; 44 + U8 vid_next_cursor[512]; 45 + I64 vid_exhausted; 46 + 47 + // Zero the video list state WITHOUT attempting to free. This is for 48 + // Browse() *entry* — the REPL's globals survive across `Browse;` 49 + // invocations, so if a previous run crashed mid-execution the slots 50 + // may contain half-freed, half-stale pointers that cannot be safely 51 + // freed. We trade a small leak (titles + uris from the previous 52 + // corrupted run, maybe a few KB) for guaranteed no Bad Free. 53 + U0 ClearVideoList() 54 + { 55 + I64 i; 56 + for (i = 0; i < MAX_VIDEOS_CAP; i++) { 57 + vid_titles[i] = NULL; 58 + vid_uris[i] = NULL; 59 + } 60 + vid_count = 0; 61 + vid_next_cursor[0] = 0; 62 + vid_exhausted = FALSE; 63 + } 64 + 65 + // Free the current video list and zero state. Used at Browse *exit* 66 + // where we trust the state is consistent — if Browse ran cleanly, 67 + // every populated slot holds a valid malloc'd pointer. 68 + U0 ResetVideoList() 69 + { 70 + I64 i; 71 + for (i = 0; i < vid_count; i++) { 72 + if (vid_titles[i]) { Free(vid_titles[i]); vid_titles[i] = NULL; } 73 + if (vid_uris[i]) { Free(vid_uris[i]); vid_uris[i] = NULL; } 74 + } 75 + vid_count = 0; 76 + vid_next_cursor[0] = 0; 77 + vid_exhausted = FALSE; 78 + } 79 + 80 + // Fetch one batch of videos. Appends to the existing vid_titles / 81 + // vid_uris arrays. Uses `vid_next_cursor` for pagination (empty = 82 + // initial batch). Updates `vid_next_cursor` and `vid_exhausted` on 83 + // return. Returns the number of videos added (0 if none, -1 on error). 84 + // 85 + // Batch size is capped at g_prefs.video_fetch_count AND the ATProto 86 + // per-request limit AND whatever room is left in the static arrays. 87 + I64 FetchVideoBatch() 88 + { 89 + if (vid_exhausted) return 0; 90 + if (vid_count >= MAX_VIDEOS_CAP) return 0; 91 + 92 + I64 want = g_prefs.video_fetch_count; 93 + if (want < 1) want = 1; 94 + if (want > ATPROTO_PAGE_SIZE) want = ATPROTO_PAGE_SIZE; 95 + I64 room = MAX_VIDEOS_CAP - vid_count; 96 + if (want > room) want = room; 97 + 98 + U8 url[2048]; 99 + U8 enc_cursor[1024]; 100 + if (vid_next_cursor[0]) { 101 + // Cursors can contain `:`, `/`, `+` etc. — URL-encode them. 102 + UrlEncode(enc_cursor, vid_next_cursor, 1024); 103 + StrPrint(url, 104 + "%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=%d&cursor=%s", 105 + g_prefs.pds, g_prefs.did, ATPROTO_COL, want, enc_cursor); 106 + } else { 107 + StrPrint(url, 108 + "%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=%d", 109 + g_prefs.pds, g_prefs.did, ATPROTO_COL, want); 110 + } 111 + 112 + I64 status, body_len; 113 + U8 *body; 114 + I64 rc = BFetch(url, &status, &body, &body_len); 115 + if (rc != 0) { "Fetch failed: %d\n", rc; return -1; } 116 + if (status != 200) { "HTTP %d\n", status; return -1; } 117 + 118 + U64 json = NetJsonParse(body, body_len); 119 + if (!json) { "JSON parse failed\n"; return -1; } 120 + 121 + U64 records = NetJsonArr(json, "records"); 122 + if (!records) { NetJsonFree(json); vid_exhausted = TRUE; return 0; } 123 + 124 + I64 n = NetJsonArrLen(records); 125 + I64 added = 0; 126 + I64 i; 127 + for (i = 0; i < n && vid_count < MAX_VIDEOS_CAP; i++) { 128 + U64 rec = NetJsonArrIdx(records, i); 129 + U8 *uri = NetJsonStr(rec, "uri"); 130 + U64 val = NetJsonObj(rec, "value"); 131 + U8 *title = NULL; 132 + if (val) title = NetJsonStr(val, "title"); 133 + vid_uris[vid_count] = uri; 134 + if (title) { 135 + vid_titles[vid_count] = title; 136 + } else { 137 + vid_titles[vid_count] = MAlloc(32); 138 + StrPrint(vid_titles[vid_count], "Video %d (no title)", vid_count + 1); 139 + } 140 + vid_count++; 141 + added++; 142 + } 143 + 144 + // Short response → the server has no more records. 145 + if (n < want) vid_exhausted = TRUE; 146 + 147 + // Save cursor for the next batch. 148 + U8 *next_cursor = NetJsonStr(json, "cursor"); 149 + NetJsonFree(json); 150 + 151 + if (next_cursor && next_cursor[0]) { 152 + SafeStrCpy(vid_next_cursor, next_cursor, 512); 153 + } else { 154 + vid_next_cursor[0] = 0; 155 + vid_exhausted = TRUE; 156 + } 157 + if (next_cursor) Free(next_cursor); 158 + 159 + return added; 160 + } 161 + 162 + // Initial fetch: clear any stale state (without dereferencing 163 + // potentially-stale pointers) and load the first batch. Returns 164 + // vid_count, or -1 on error. 165 + I64 FetchVideos() 166 + { 167 + ClearVideoList; 168 + "Fetching first batch...\n"; 169 + I64 added = FetchVideoBatch; 170 + if (added < 0) return -1; 171 + "%d videos loaded\n", vid_count; 172 + if (vid_count == 0) return -1; 173 + return vid_count; 174 + }
+111
holyc/vodbrowser/lib/Load.HC
··· 1 + // Load.HC - ELF .prg loader for TempleOS 2 + #ifndef LOAD_HC 3 + #define LOAD_HC 4 + 5 + // Set to TRUE by worker tasks to suppress init prints 6 + I64 elf_quiet; 7 + 8 + // Second parameter is the opaque init arg passed to the ELF's 9 + // entry point in RDI. Declared without a name because we read it via 10 + // SF_ARG2[RBP] in the asm block below, not by name — naming it causes 11 + // HolyC to emit a spurious "Unused var" warning. 12 + // 13 + // g_vod_dir holds the absolute "letter:/dir" path of the directory 14 + // VodBrowser was loaded from. BrowseInit fills it in from the main 15 + // task's cur_dv + cur_dir before spawning NetTask / DecodeTask, and 16 + // NetInit / MediaInit read it to build absolute paths for ElfLoad. 17 + // We can't rely on the spawned task's own cur_dir (TempleOS Spawn 18 + // resets every child to /Home — see Kernel/KTask.HC) or on relative 19 + // paths in ElfLoad, so the global is the simplest shared-state 20 + // escape hatch from parent to worker. 21 + U8 g_vod_dir[256]; 22 + U64 ElfLoad(U8 *filename, U64) 23 + { 24 + I64 file_size; 25 + U8 *file_data; 26 + U64 *hdr; 27 + U64 hdr_start; 28 + U64 hdr_end; 29 + U64 hdr_load_size; 30 + U64 hdr_total_size; 31 + U64 i; 32 + U32 reloc; 33 + U64 val; 34 + 35 + file_data = FileRead(filename, &file_size); 36 + if (!file_data) { 37 + "ElfLoad: can't read '%s'\n", filename; 38 + return 0; 39 + } 40 + 41 + hdr = file_data; 42 + hdr_start = hdr[0]; 43 + hdr_end = hdr[4]; 44 + hdr_load_size = hdr[5]; 45 + hdr_total_size = hdr[6]; 46 + 47 + if ((hdr_start >> 32) != 0x77646a00) { 48 + "ElfLoad: bad magic\n"; 49 + Free(file_data); 50 + return 0; 51 + } 52 + 53 + // Allocate ELF buffer. TempleOS has no NX bit — all memory is 54 + // executable (ring 0, identity-mapped). Use default MAlloc which 55 + // draws from whichever pool has space. 56 + U64 alloc_size = hdr_end; 57 + U8 *buf = MAlloc(alloc_size); 58 + MemCpy(buf, file_data, file_size); 59 + if (hdr_end > file_size) 60 + MemSet(buf + file_size, 0, hdr_end - file_size); 61 + Free(file_data); 62 + 63 + // Relocations 64 + I64 cnt = 0; 65 + I64 bad = 0; 66 + i = hdr_load_size; 67 + while (i < hdr_total_size) { 68 + MemCpy(&reloc, buf + i, 4); 69 + if (reloc * 8 + 8 > hdr_end) { 70 + "BAD RELOC[%d]: idx=%d offset=0x%X (end=0x%X)\n", cnt, reloc, reloc * 8, hdr_end; 71 + bad++; 72 + } else { 73 + MemCpy(&val, buf + reloc * 8, 8); 74 + val = val + buf; 75 + MemCpy(buf + reloc * 8, &val, 8); 76 + } 77 + cnt++; 78 + i = i + 4; 79 + } 80 + if (bad) "WARNING: %d bad relocations!\n", bad; 81 + MemSet(buf + hdr_load_size, 0, hdr_total_size - hdr_load_size); 82 + 83 + if (!elf_quiet) 84 + "ElfLoad: %d bytes, %d relocs, buf=0x%X\n", file_size, cnt, buf; 85 + 86 + // Call entry with arg in RDI 87 + I64 noreg result; 88 + U64 noreg entry = buf; 89 + asm { 90 + PUSH RBX 91 + PUSH R10 92 + PUSH R11 93 + PUSH RSI 94 + PUSH RDI 95 + MOV RDI, U64 SF_ARG2[RBP] 96 + MOV RAX, U64 &entry[RBP] 97 + CALL RAX 98 + MOV U64 &result[RBP], RAX 99 + POP RDI 100 + POP RSI 101 + POP R11 102 + POP R10 103 + POP RBX 104 + }; 105 + 106 + return result; 107 + } 108 + 109 + #include "TosCallbacks.HC" 110 + 111 + #endif // LOAD_HC
+238
holyc/vodbrowser/lib/Media.HC
··· 1 + #ifndef MEDIA_HC 2 + #define MEDIA_HC 3 + // Media.HC - HolyC wrapper for Media.prg (H.264 video decoder) 4 + // 5 + // Uses a single generic machine-code thunk for all C calls, 6 + // matching the proven pattern from TestFetch.HC / Net.HC. 7 + 8 + #include "Load.HC" 9 + 10 + // Function table offsets — must match struct media_api in media_api.h. 11 + #define MEDIA_FN_OPEN 0 12 + #define MEDIA_FN_CLOSE 1 13 + #define MEDIA_FN_VIDEO_INFO 2 14 + #define MEDIA_FN_NEXT 3 15 + #define MEDIA_FN_GET_RGB 4 16 + #define MEDIA_FN_GET_PAL 5 17 + #define MEDIA_FN_NEXT_PAL 6 18 + #define MEDIA_FN_SET_DITHER_MODE 7 19 + #define MEDIA_FN_GET_PTS 8 20 + #define MEDIA_FN_SET_SKIP_NON_REF 9 21 + #define MEDIA_FN_GET_SKIP_COUNT 10 22 + 23 + // Frame types (must match media_api.h) 24 + #define MEDIA_VIDEO 1 25 + #define MEDIA_EOF 0 26 + #define MEDIA_ERROR -1 27 + 28 + // Dither modes (must match media_api.h MEDIA_DITHER_* constants) 29 + #define DITHER_FLOYD 0 30 + #define DITHER_NEAREST 1 31 + #define DITHER_GRAY 2 32 + 33 + U64 *media_api_table; 34 + 35 + // ---- Generic C-call thunk (up to 4 args via globals) ---- 36 + // Set g_cfn/g_a1-4 then Call(_THUNK_CCALL). Result in RAX. 37 + 38 + U8 *_THUNK_CCALL; 39 + U64 g_cfn; 40 + U64 g_a1, g_a2, g_a3, g_a4; 41 + 42 + U0 MediaBuildThunk() 43 + { 44 + I64 i = 0; 45 + U64 addr; 46 + 47 + _THUNK_CCALL = MAlloc(128, Fs->code_heap); 48 + 49 + // Save HolyC callee-saved regs that C may clobber 50 + _THUNK_CCALL[i++] = 0x56; // PUSH RSI 51 + _THUNK_CCALL[i++] = 0x57; // PUSH RDI 52 + _THUNK_CCALL[i++] = 0x41; _THUNK_CCALL[i++] = 0x52; // PUSH R10 53 + _THUNK_CCALL[i++] = 0x41; _THUNK_CCALL[i++] = 0x53; // PUSH R11 54 + 55 + // Load args from globals into C ABI registers 56 + // MOV RAX,[g_a1]; MOV RDI,RAX 57 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0xA1; 58 + addr = &g_a1; MemCpy(_THUNK_CCALL + i, &addr, 8); i += 8; 59 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0x89; _THUNK_CCALL[i++] = 0xC7; 60 + 61 + // MOV RAX,[g_a2]; MOV RSI,RAX 62 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0xA1; 63 + addr = &g_a2; MemCpy(_THUNK_CCALL + i, &addr, 8); i += 8; 64 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0x89; _THUNK_CCALL[i++] = 0xC6; 65 + 66 + // MOV RAX,[g_a3]; MOV RDX,RAX 67 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0xA1; 68 + addr = &g_a3; MemCpy(_THUNK_CCALL + i, &addr, 8); i += 8; 69 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0x89; _THUNK_CCALL[i++] = 0xC2; 70 + 71 + // MOV RAX,[g_a4]; MOV RCX,RAX 72 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0xA1; 73 + addr = &g_a4; MemCpy(_THUNK_CCALL + i, &addr, 8); i += 8; 74 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0x89; _THUNK_CCALL[i++] = 0xC1; 75 + 76 + // MOV RAX,[g_cfn]; CALL RAX 77 + _THUNK_CCALL[i++] = 0x48; _THUNK_CCALL[i++] = 0xA1; 78 + addr = &g_cfn; MemCpy(_THUNK_CCALL + i, &addr, 8); i += 8; 79 + _THUNK_CCALL[i++] = 0xFF; _THUNK_CCALL[i++] = 0xD0; 80 + 81 + // Restore 82 + _THUNK_CCALL[i++] = 0x41; _THUNK_CCALL[i++] = 0x5B; // POP R11 83 + _THUNK_CCALL[i++] = 0x41; _THUNK_CCALL[i++] = 0x5A; // POP R10 84 + _THUNK_CCALL[i++] = 0x5F; // POP RDI 85 + _THUNK_CCALL[i++] = 0x5E; // POP RSI 86 + _THUNK_CCALL[i++] = 0xC3; // RET 87 + 88 + if (!elf_quiet) "Media: thunk at 0x%X (%d bytes)\n", _THUNK_CCALL, i; 89 + } 90 + 91 + // ---- Media API wrappers ---- 92 + // Call(thunk) MUST be the only statement in the function body. 93 + // Mixing it with global assignments in the same function causes 94 + // HolyC register allocation to clobber values across the Call. 95 + 96 + I64 DoThunkCall() { return Call(_THUNK_CCALL); } 97 + 98 + U64 MediaOpen(U8 *data, I64 len) 99 + { 100 + g_cfn = media_api_table[MEDIA_FN_OPEN]; 101 + g_a1 = data; g_a2 = len; 102 + return DoThunkCall; 103 + } 104 + 105 + U0 MediaClose(U64 ctx) 106 + { 107 + g_cfn = media_api_table[MEDIA_FN_CLOSE]; 108 + g_a1 = ctx; 109 + DoThunkCall; 110 + } 111 + 112 + I64 MediaVideoInfo(U64 ctx, I64 *w, I64 *h) 113 + { 114 + g_cfn = media_api_table[MEDIA_FN_VIDEO_INFO]; 115 + g_a1 = ctx; g_a2 = w; g_a3 = h; 116 + return DoThunkCall; 117 + } 118 + 119 + I64 MediaNext(U64 ctx) 120 + { 121 + g_cfn = media_api_table[MEDIA_FN_NEXT]; 122 + g_a1 = ctx; 123 + return DoThunkCall; 124 + } 125 + 126 + I64 MediaGetRGB(U64 ctx, U8 *rgb, I64 *w, I64 *h) 127 + { 128 + g_cfn = media_api_table[MEDIA_FN_GET_RGB]; 129 + g_a1 = ctx; g_a2 = rgb; g_a3 = w; g_a4 = h; 130 + return DoThunkCall; 131 + } 132 + 133 + I64 MediaGetPal(U64 ctx, U8 *pal, I64 *w, I64 *h) 134 + { 135 + g_cfn = media_api_table[MEDIA_FN_GET_PAL]; 136 + g_a1 = ctx; g_a2 = pal; g_a3 = w; g_a4 = h; 137 + return DoThunkCall; 138 + } 139 + 140 + I64 MediaNextPal(U64 ctx, U8 *pal, I64 *w, I64 *h) 141 + { 142 + g_cfn = media_api_table[MEDIA_FN_NEXT_PAL]; 143 + g_a1 = ctx; g_a2 = pal; g_a3 = w; g_a4 = h; 144 + return DoThunkCall; 145 + } 146 + 147 + // Select the dither mode used by media_next_pal. `mode` is one of 148 + // DITHER_FLOYD / DITHER_NEAREST / DITHER_GRAY. Safe to call from the 149 + // main task while DecodeTask is idle (menu, between playbacks) — the 150 + // C side just stores the value in a static int. 151 + U0 MediaSetDitherMode(I64 mode) 152 + { 153 + g_cfn = media_api_table[MEDIA_FN_SET_DITHER_MODE]; 154 + g_a1 = mode; 155 + DoThunkCall; 156 + } 157 + 158 + // Presentation timestamp of the last decoded frame, in milliseconds 159 + // from the track origin. Only meaningful after MediaNext returned 160 + // MEDIA_VIDEO. Returns 0 if the track timescale was unknown, in 161 + // which case DecodeTask should skip PTS pacing for that segment. 162 + I64 MediaGetPts(U64 ctx) 163 + { 164 + g_cfn = media_api_table[MEDIA_FN_GET_PTS]; 165 + g_a1 = ctx; 166 + return DoThunkCall; 167 + } 168 + 169 + // Master switch for dynamic non-reference frame skipping on the 170 + // C side. When enabled, Media.prg's on_frame compares each 171 + // frame's PTS against wallclock and drops non-reference H.264 172 + // frames only when we're actually falling behind. When disabled, 173 + // every frame is decoded regardless of timing. This is called 174 + // from BrowseInit and Settings save to push the g_prefs.skip_non_ref 175 + // value into Media.prg — the per-frame skip decision is made 176 + // dynamically inside on_frame. 177 + U0 MediaSetSkipNonRef(I64 enabled) 178 + { 179 + g_cfn = media_api_table[MEDIA_FN_SET_SKIP_NON_REF]; 180 + g_a1 = enabled; 181 + DoThunkCall; 182 + } 183 + 184 + // Cumulative count of non-reference frames the dynamic pacing 185 + // logic has dropped since MediaOpen. Read by DecodeTask at the 186 + // end of each segment so the post-playback summary can report 187 + // the real drop count. 188 + I64 MediaGetSkipCount(U64 ctx) 189 + { 190 + g_cfn = media_api_table[MEDIA_FN_GET_SKIP_COUNT]; 191 + g_a1 = ctx; 192 + return DoThunkCall; 193 + } 194 + 195 + // ---- Initialization ---- 196 + 197 + I64 MediaInit() 198 + { 199 + // Build the generic C-call thunk 200 + MediaBuildThunk; 201 + 202 + // Build init args: [0]=tos_malloc, [1]=tos_free, [2]=tos_ticks_ms. 203 + // _TOS_TICKS feeds sys_now() inside Media.prg's minilib so the 204 + // DecodeTask pacing logic can read a wallclock from C if it ever 205 + // needs to. (HolyC uses cnts.jiffies directly for pacing today, 206 + // but keeping sys_now live means future C-side timing just works.) 207 + U64 *init_args = CAlloc(24); 208 + init_args[0] = &_TOS_MALLOC; 209 + init_args[1] = &_TOS_FREE; 210 + init_args[2] = &_TOS_TICKS; 211 + 212 + // Same g_vod_dir trick as Net.HC's ElfLoad — see the comment 213 + // there for the full rationale. BrowseInit captures the main 214 + // task's absolute cur_dir before spawning DecodeTask; this runs 215 + // on the DecodeTask side and pulls that captured path back out 216 + // so ElfLoad gets a drive-qualified absolute path. 217 + // g_vod_dir ends with '/' by contract (BrowseInit normalises it) 218 + // so we concatenate directly — see the comment in Net.HC. 219 + U8 prg_path[256]; 220 + StrPrint(prg_path, "%sMedia.prg", g_vod_dir); 221 + U64 result = ElfLoad(prg_path, init_args); 222 + if (!result || result < 0x1000) { 223 + "MediaInit: ElfLoad failed (%d)\n", result; 224 + Free(init_args); 225 + return -1; 226 + } 227 + 228 + media_api_table = result; 229 + if (!elf_quiet) { 230 + "MediaInit: API table at 0x%X\n", media_api_table; 231 + " open = 0x%X\n", media_api_table[0]; 232 + " next = 0x%X\n", media_api_table[4]; 233 + } 234 + 235 + return 0; 236 + } 237 + 238 + #endif // MEDIA_HC
+439
holyc/vodbrowser/lib/Net.HC
··· 1 + #ifndef NET_HC 2 + #define NET_HC 3 + // Net.HC - HolyC wrapper for Net.prg (network ELF binary) 4 + // 5 + // Loads the networking ELF (BearSSL + lwIP + picohttpparser + jsmn), 6 + // passes NIC driver function pointers, and exposes fetch/json API. 7 + 8 + #include "Load.HC" 9 + #include "Nic.HC" 10 + 11 + // Function table offsets (must match struct net_api in net_api.h) 12 + #define NET_FN_FETCH 0 13 + #define NET_FN_FETCH_RANGE 1 14 + #define NET_FN_FREE_RESPONSE 2 15 + #define NET_FN_JSON_PARSE 3 16 + #define NET_FN_JSON_FREE 4 17 + #define NET_FN_JSON_STR 5 18 + #define NET_FN_JSON_OBJ 6 19 + #define NET_FN_JSON_ARR 7 20 + #define NET_FN_JSON_ARR_LEN 8 21 + #define NET_FN_JSON_ARR_IDX 9 22 + #define NET_FN_JSON_IS_STR 10 23 + #define NET_FN_JSON_IS_ARR 11 24 + #define NET_FN_JSON_IS_OBJ 12 25 + #define NET_FN_JSON_STRVAL 13 26 + #define NET_FN_POLL 14 27 + #define NET_FN_GET_IP 15 28 + #define NET_FN_FETCH_BEGIN 16 29 + #define NET_FN_FETCH_TICK 17 30 + #define NET_FN_FETCH_RESULT 18 31 + #define NET_FN_SET_HEADERS 19 32 + #define NET_FN_GET_LAST_REUSED 20 33 + 34 + // Async fetch states (must match net_api.h) 35 + #define NET_FETCH_IDLE 0 36 + #define NET_FETCH_PENDING 1 37 + #define NET_FETCH_DONE 2 38 + #define NET_FETCH_ERROR -1 39 + 40 + U64 *net_api_table; // Array of 16 function pointers 41 + 42 + // Thunk globals (set during NetInit, used by wrappers) 43 + U8 *_THUNK_POLL; 44 + U8 *_THUNK_GETIP; 45 + U8 *_THUNK_FETCH; 46 + U64 g_fetch_url; 47 + U64 g_fetch_resp; 48 + 49 + // ---- Generic C-call thunks ---- 50 + // These save HolyC callee-saved regs (RSI, RDI), set up C ABI args, call, 51 + // restore. Parameters are declared without names because the inline asm 52 + // reads them from the HolyC stack frame by offset (SF_ARG1[RBP] etc.), 53 + // not by name — naming them triggers "unused var" warnings. 54 + 55 + #ifndef CCALL_DEFINED 56 + #define CCALL_DEFINED 57 + 58 + // 0-arg C call: fn() 59 + I64 CCall0(U64) 60 + { 61 + I64 noreg r; 62 + asm { 63 + PUSH RBX 64 + PUSH R10 65 + PUSH R11 66 + PUSH RSI 67 + PUSH RDI 68 + MOV RAX, U64 SF_ARG1[RBP] 69 + CALL RAX 70 + MOV U64 &r[RBP], RAX 71 + POP RDI 72 + POP RSI 73 + POP R11 74 + POP R10 75 + POP RBX 76 + }; 77 + return r; 78 + } 79 + 80 + // 1-arg C call: fn(arg1) 81 + I64 CCall1(U64, U64) 82 + { 83 + I64 noreg r; 84 + asm { 85 + PUSH RBX 86 + PUSH R10 87 + PUSH R11 88 + PUSH RSI 89 + PUSH RDI 90 + MOV RDI, U64 SF_ARG2[RBP] 91 + MOV RAX, U64 SF_ARG1[RBP] 92 + CALL RAX 93 + MOV U64 &r[RBP], RAX 94 + POP RDI 95 + POP RSI 96 + POP R11 97 + POP R10 98 + POP RBX 99 + }; 100 + return r; 101 + } 102 + 103 + // 2-arg C call: fn(arg1, arg2) 104 + I64 CCall2(U64, U64, U64) 105 + { 106 + I64 noreg r; 107 + asm { 108 + PUSH RBX 109 + PUSH R10 110 + PUSH R11 111 + PUSH RSI 112 + PUSH RDI 113 + MOV RDI, U64 SF_ARG2[RBP] 114 + MOV RSI, U64 SF_ARG3[RBP] 115 + MOV RAX, U64 SF_ARG1[RBP] 116 + CALL RAX 117 + MOV U64 &r[RBP], RAX 118 + POP RDI 119 + POP RSI 120 + POP R11 121 + POP R10 122 + POP RBX 123 + }; 124 + return r; 125 + } 126 + 127 + // 3-arg C call: fn(arg1, arg2, arg3) 128 + I64 CCall3(U64, U64, U64, U64) 129 + { 130 + I64 noreg r; 131 + asm { 132 + PUSH RBX 133 + PUSH R10 134 + PUSH R11 135 + PUSH RSI 136 + PUSH RDI 137 + MOV RDI, U64 SF_ARG2[RBP] 138 + MOV RSI, U64 SF_ARG3[RBP] 139 + MOV RDX, U64 SF_ARG4[RBP] 140 + MOV RAX, U64 SF_ARG1[RBP] 141 + CALL RAX 142 + MOV U64 &r[RBP], RAX 143 + POP RDI 144 + POP RSI 145 + POP R11 146 + POP R10 147 + POP RBX 148 + }; 149 + return r; 150 + } 151 + 152 + // 4-arg C call: fn(arg1, arg2, arg3, arg4) 153 + I64 CCall4(U64, U64, U64, U64, U64) 154 + { 155 + I64 noreg r; 156 + asm { 157 + PUSH RBX 158 + PUSH R10 159 + PUSH R11 160 + PUSH RSI 161 + PUSH RDI 162 + MOV RDI, U64 SF_ARG2[RBP] 163 + MOV RSI, U64 SF_ARG3[RBP] 164 + MOV RDX, U64 SF_ARG4[RBP] 165 + MOV RCX, U64 SF_ARG5[RBP] 166 + MOV RAX, U64 SF_ARG1[RBP] 167 + CALL RAX 168 + MOV U64 &r[RBP], RAX 169 + POP RDI 170 + POP RSI 171 + POP R11 172 + POP R10 173 + POP RBX 174 + }; 175 + return r; 176 + } 177 + 178 + #endif // CCALL_DEFINED 179 + 180 + // ---- Net API wrappers ---- 181 + 182 + // Fetch thunk must be called alone (see Media.HC pattern) 183 + I64 DoFetchCall() { return Call(_THUNK_FETCH); } 184 + 185 + // Fetch(url, &resp) -> int 186 + I64 NetFetch(U8 *url, U8 *resp) 187 + { 188 + g_fetch_url = url; 189 + g_fetch_resp = resp; 190 + return DoFetchCall; 191 + } 192 + 193 + // FetchRange(url, start, end, &resp) -> int 194 + I64 NetFetchRange(U8 *url, I64 start, I64 end, U8 *resp) 195 + { 196 + return CCall4(net_api_table[NET_FN_FETCH_RANGE], url, start, end, resp); 197 + } 198 + 199 + // FreeResponse(&resp) 200 + U0 NetFreeResp(U8 *resp) 201 + { 202 + CCall1(net_api_table[NET_FN_FREE_RESPONSE], resp); 203 + } 204 + 205 + // Async fetch API — non-blocking state machine 206 + I64 NetFetchBegin(U8 *url) 207 + { 208 + return CCall1(net_api_table[NET_FN_FETCH_BEGIN], url); 209 + } 210 + 211 + I64 NetFetchTick() 212 + { 213 + return CCall0(net_api_table[NET_FN_FETCH_TICK]); 214 + } 215 + 216 + I64 NetFetchResult(U8 *resp) 217 + { 218 + return CCall1(net_api_table[NET_FN_FETCH_RESULT], resp); 219 + } 220 + 221 + // Set extra headers for next async fetch (e.g. Range) 222 + U0 NetSetHeaders(U8 *hdrs) 223 + { 224 + CCall1(net_api_table[NET_FN_SET_HEADERS], hdrs); 225 + } 226 + 227 + // Returns 1 if the most recent NetFetchBegin reused an existing TLS 228 + // connection, 0 if it opened a fresh one. Diagnostic for keep-alive. 229 + I64 NetGetLastReused() 230 + { 231 + return CCall0(net_api_table[NET_FN_GET_LAST_REUSED]); 232 + } 233 + 234 + // JsonParse(data, len) -> handle 235 + U64 NetJsonParse(U8 *data, I64 len) 236 + { 237 + return CCall2(net_api_table[NET_FN_JSON_PARSE], data, len); 238 + } 239 + 240 + // JsonFree(handle) 241 + U0 NetJsonFree(U64 json) 242 + { 243 + CCall1(net_api_table[NET_FN_JSON_FREE], json); 244 + } 245 + 246 + // JsonStr(handle, key) -> string 247 + U8 *NetJsonStr(U64 json, U8 *key) 248 + { 249 + return CCall2(net_api_table[NET_FN_JSON_STR], json, key); 250 + } 251 + 252 + // JsonObj(handle, key) -> handle 253 + U64 NetJsonObj(U64 json, U8 *key) 254 + { 255 + return CCall2(net_api_table[NET_FN_JSON_OBJ], json, key); 256 + } 257 + 258 + // JsonArr(handle, key) -> handle 259 + U64 NetJsonArr(U64 json, U8 *key) 260 + { 261 + return CCall2(net_api_table[NET_FN_JSON_ARR], json, key); 262 + } 263 + 264 + // JsonArrLen(handle) -> int 265 + I64 NetJsonArrLen(U64 json) 266 + { 267 + return CCall1(net_api_table[NET_FN_JSON_ARR_LEN], json); 268 + } 269 + 270 + // JsonArrIdx(handle, idx) -> handle 271 + U64 NetJsonArrIdx(U64 json, I64 idx) 272 + { 273 + return CCall2(net_api_table[NET_FN_JSON_ARR_IDX], json, idx); 274 + } 275 + 276 + // JsonIsStr(handle) -> bool 277 + I64 NetJsonIsStr(U64 json) { return CCall1(net_api_table[NET_FN_JSON_IS_STR], json); } 278 + I64 NetJsonIsArr(U64 json) { return CCall1(net_api_table[NET_FN_JSON_IS_ARR], json); } 279 + I64 NetJsonIsObj(U64 json) { return CCall1(net_api_table[NET_FN_JSON_IS_OBJ], json); } 280 + 281 + // JsonStrVal(handle) -> string 282 + U8 *NetJsonStrVal(U64 json) 283 + { 284 + return CCall1(net_api_table[NET_FN_JSON_STRVAL], json); 285 + } 286 + 287 + // ---- Machine-code thunks for hot-path 0-arg calls ---- 288 + // These use the same proven pattern as TestFetch.HC's thunks. 289 + // Built during NetInit, called via Call() so the HolyC compiler 290 + // generates proper IC_CALL_START/IC_CALL_END register saving. 291 + 292 + U0 NetBuildThunks() 293 + { 294 + I64 i; 295 + U64 addr; 296 + 297 + // Poll thunk: save regs, call [poll_fn], restore, RET 298 + _THUNK_POLL = MAlloc(64, Fs->code_heap); 299 + i = 0; 300 + _THUNK_POLL[i++] = 0x56; // PUSH RSI 301 + _THUNK_POLL[i++] = 0x57; // PUSH RDI 302 + _THUNK_POLL[i++] = 0x41; _THUNK_POLL[i++] = 0x52; // PUSH R10 303 + _THUNK_POLL[i++] = 0x41; _THUNK_POLL[i++] = 0x53; // PUSH R11 304 + addr = net_api_table[NET_FN_POLL]; 305 + _THUNK_POLL[i++] = 0x48; _THUNK_POLL[i++] = 0xB8; // MOV RAX, imm64 306 + MemCpy(_THUNK_POLL + i, &addr, 8); i += 8; 307 + _THUNK_POLL[i++] = 0xFF; _THUNK_POLL[i++] = 0xD0; // CALL RAX 308 + _THUNK_POLL[i++] = 0x41; _THUNK_POLL[i++] = 0x5B; // POP R11 309 + _THUNK_POLL[i++] = 0x41; _THUNK_POLL[i++] = 0x5A; // POP R10 310 + _THUNK_POLL[i++] = 0x5F; // POP RDI 311 + _THUNK_POLL[i++] = 0x5E; // POP RSI 312 + _THUNK_POLL[i++] = 0xC3; // RET 313 + 314 + // Fetch thunk: loads url/resp from globals into RDI/RSI, calls fetch 315 + _THUNK_FETCH = MAlloc(96, Fs->code_heap); 316 + i = 0; 317 + _THUNK_FETCH[i++] = 0x56; // PUSH RSI 318 + _THUNK_FETCH[i++] = 0x57; // PUSH RDI 319 + _THUNK_FETCH[i++] = 0x41; _THUNK_FETCH[i++] = 0x52; // PUSH R10 320 + _THUNK_FETCH[i++] = 0x41; _THUNK_FETCH[i++] = 0x53; // PUSH R11 321 + // MOV RAX, [g_fetch_url]; MOV RDI, RAX 322 + _THUNK_FETCH[i++] = 0x48; _THUNK_FETCH[i++] = 0xA1; 323 + addr = &g_fetch_url; 324 + MemCpy(_THUNK_FETCH + i, &addr, 8); i += 8; 325 + _THUNK_FETCH[i++] = 0x48; _THUNK_FETCH[i++] = 0x89; _THUNK_FETCH[i++] = 0xC7; // MOV RDI, RAX 326 + // MOV RAX, [g_fetch_resp]; MOV RSI, RAX 327 + _THUNK_FETCH[i++] = 0x48; _THUNK_FETCH[i++] = 0xA1; 328 + addr = &g_fetch_resp; 329 + MemCpy(_THUNK_FETCH + i, &addr, 8); i += 8; 330 + _THUNK_FETCH[i++] = 0x48; _THUNK_FETCH[i++] = 0x89; _THUNK_FETCH[i++] = 0xC6; // MOV RSI, RAX 331 + // MOV RAX, fetch_fn; CALL RAX 332 + addr = net_api_table[NET_FN_FETCH]; 333 + _THUNK_FETCH[i++] = 0x48; _THUNK_FETCH[i++] = 0xB8; 334 + MemCpy(_THUNK_FETCH + i, &addr, 8); i += 8; 335 + _THUNK_FETCH[i++] = 0xFF; _THUNK_FETCH[i++] = 0xD0; // CALL RAX 336 + _THUNK_FETCH[i++] = 0x41; _THUNK_FETCH[i++] = 0x5B; // POP R11 337 + _THUNK_FETCH[i++] = 0x41; _THUNK_FETCH[i++] = 0x5A; // POP R10 338 + _THUNK_FETCH[i++] = 0x5F; // POP RDI 339 + _THUNK_FETCH[i++] = 0x5E; // POP RSI 340 + _THUNK_FETCH[i++] = 0xC3; // RET 341 + 342 + // GetIp thunk: same pattern, returns value in RAX 343 + _THUNK_GETIP = MAlloc(64, Fs->code_heap); 344 + i = 0; 345 + _THUNK_GETIP[i++] = 0x56; 346 + _THUNK_GETIP[i++] = 0x57; 347 + _THUNK_GETIP[i++] = 0x41; _THUNK_GETIP[i++] = 0x52; 348 + _THUNK_GETIP[i++] = 0x41; _THUNK_GETIP[i++] = 0x53; 349 + addr = net_api_table[NET_FN_GET_IP]; 350 + _THUNK_GETIP[i++] = 0x48; _THUNK_GETIP[i++] = 0xB8; 351 + MemCpy(_THUNK_GETIP + i, &addr, 8); i += 8; 352 + _THUNK_GETIP[i++] = 0xFF; _THUNK_GETIP[i++] = 0xD0; 353 + _THUNK_GETIP[i++] = 0x41; _THUNK_GETIP[i++] = 0x5B; 354 + _THUNK_GETIP[i++] = 0x41; _THUNK_GETIP[i++] = 0x5A; 355 + _THUNK_GETIP[i++] = 0x5F; 356 + _THUNK_GETIP[i++] = 0x5E; 357 + _THUNK_GETIP[i++] = 0xC3; 358 + } 359 + 360 + // Poll - must be called periodically to drive lwIP 361 + U0 NetPoll() { Call(_THUNK_POLL); } 362 + 363 + // Get current IP address (0 = no IP yet, still doing DHCP) 364 + U32 NetGetIp() { return Call(_THUNK_GETIP); } 365 + 366 + // ---- Initialization ---- 367 + 368 + // Must match struct net_init_args in net_entry.c (56 bytes) 369 + // [0] send_fn (8) 370 + // [1] recv_fn (8) 371 + // [2] mac[6]+pad[2] (8) 372 + // [3] rand_u64 (8) 373 + // [4] get_ticks (8) 374 + // [5] tos_malloc (8) 375 + // [6] tos_free (8) 376 + 377 + I64 NetInit() 378 + { 379 + // Initialize NIC hardware 380 + if (NicInit < 0) { 381 + "NetInit: NIC init failed\n"; 382 + return -1; 383 + } 384 + 385 + // Build C-callable trampolines for NIC functions 386 + NicBuildTrampolines; 387 + 388 + // Build init args — must match struct net_init_args in net_entry.c 389 + U8 *init_args = CAlloc(64); 390 + U64 *a = init_args; 391 + a[0] = _NIC_SEND_C; // C-callable trampoline, NOT raw &NicSend 392 + a[1] = _NIC_RECV_C; 393 + MemCpy(init_args + 16, nic_mac, 6); 394 + a[3] = &_TOS_RAND; 395 + a[4] = &_TOS_TICKS; 396 + a[5] = &_TOS_MALLOC; 397 + a[6] = &_TOS_FREE; 398 + 399 + // Load the Net ELF. We can't use a relative path here because 400 + // TempleOS's Spawn resets every child task's cur_dir to "/Home" 401 + // (Kernel/KTask.HC line 237 — no parent inheritance), so by the 402 + // time NetTaskFn runs on its own task the "Net.prg is next to 403 + // Net.HC" context is gone. And we can't use __DIR__ in a format 404 + // arg — on the flat sources.iso dev ISO the parser-time dir is 405 + // the drive root and __DIR__ expands to something the expression 406 + // parser chokes on with "Missing expression at )". 407 + // 408 + // Instead we use a global g_vod_dir that BrowseInit fills in with 409 + // the MAIN task's absolute "letter:/path" cur_dir before spawning 410 + // the workers. See BrowseInit in VodBrowser.HC. That gives us a 411 + // full drive-qualified path in both the sources.iso workflow 412 + // (T:/...) and the distro (T:/VodBrowser on a live CD, C:/Home/... 413 + // after install). 414 + // g_vod_dir is guaranteed to end with '/' (BrowseInit normalises 415 + // it) so we concatenate directly without inserting another slash. 416 + // That avoids "T://Net.prg" in the root-of-drive case. 417 + U8 prg_path[256]; 418 + StrPrint(prg_path, "%sNet.prg", g_vod_dir); 419 + U64 result = ElfLoad(prg_path, init_args); 420 + if (!result || result < 0x1000) { 421 + "NetInit: ElfLoad failed (%d)\n", result; 422 + Free(init_args); 423 + return -1; 424 + } 425 + 426 + net_api_table = result; 427 + if (!elf_quiet) { 428 + "NetInit: API table at 0x%X\n", net_api_table; 429 + " fetch = 0x%X\n", net_api_table[0]; 430 + " poll = 0x%X\n", net_api_table[14]; 431 + } 432 + 433 + // Build machine-code thunks for poll/getip (hot path) 434 + NetBuildThunks; 435 + 436 + return 0; 437 + } 438 + 439 + #endif // NET_HC
+276
holyc/vodbrowser/lib/Nic.HC
··· 1 + #ifndef NIC_HC 2 + #define NIC_HC 3 + // Nic.HC - PCnet-PCI II (Am79C970A) network driver for TempleOS 4 + // 5 + // SWSTYLE 2 (32-bit) descriptor format from Linux pcnet32.c: 6 + // RX/TX descriptor: base(4) + buf_length(2) + status(2) + msg_length(4) + reserved(4) = 16 bytes 7 + // Init block: mode(2) + tlen_rlen(2) + mac(6) + reserved(2) + filter(8) + rx_ring(4) + tx_ring(4) = 28 bytes 8 + 9 + #define PCNET_VENDOR_ID 0x1022 10 + #define PCNET_DEVICE_ID 0x2000 11 + 12 + // ---- PCI ---- 13 + 14 + U16 nic_io_base; 15 + 16 + U16 PcnetFindDevice() 17 + { 18 + I64 bus, dev, func; 19 + for (bus = 0; bus < 8; bus++) 20 + for (dev = 0; dev < 32; dev++) 21 + for (func = 0; func < 8; func++) { 22 + U32 id = PCIReadU32(bus, dev, func, 0); 23 + if ((id & 0xFFFF) == PCNET_VENDOR_ID && (id >> 16) == PCNET_DEVICE_ID) { 24 + U32 bar0 = PCIReadU32(bus, dev, func, 0x10); 25 + if (bar0 & 1) { 26 + if (!elf_quiet) "Nic: PCnet at %d:%d.%d io=0x%X\n", bus, dev, func, bar0 & 0xFFFC; 27 + U16 cmd = PCIReadU16(bus, dev, func, 0x04); 28 + PCIWriteU16(bus, dev, func, 0x04, cmd | 0x05); // bus master + io 29 + return bar0 & 0xFFFC; 30 + } 31 + } 32 + } 33 + return 0; 34 + } 35 + 36 + // ---- Register access (16-bit I/O mode) ---- 37 + 38 + U0 PcnetWriteCSR(U16 csr, U16 val) { OutU16(nic_io_base + 0x12, csr); OutU16(nic_io_base + 0x10, val); } 39 + U16 PcnetReadCSR(U16 csr) { OutU16(nic_io_base + 0x12, csr); return InU16(nic_io_base + 0x10); } 40 + U0 PcnetWriteBCR(U16 csr, U16 val) { OutU16(nic_io_base + 0x12, csr); OutU16(nic_io_base + 0x16, val); } 41 + U16 PcnetReadBCR(U16 csr) { OutU16(nic_io_base + 0x12, csr); return InU16(nic_io_base + 0x16); } 42 + U0 PcnetReset() { InU16(nic_io_base + 0x14); I64 i; for(i=0;i<100000;i++); } 43 + 44 + // ---- Descriptors (SWSTYLE 2, 16 bytes each) ---- 45 + // Must match hardware layout exactly: 46 + // [0:3] base - buffer physical address (U32) 47 + // [4:5] buf_length - two's complement of buffer size (I16) 48 + // [6:7] status - OWN=bit15, ERR=bit14, STP=bit9, ENP=bit8 (U16) 49 + // [8:11] msg_length - received message byte count (U32) 50 + // [12:15] reserved 51 + 52 + // NUM_RX bumped 16 → 64 to match lwIP's TCP_WND of 64*MSS (~93 KB). 53 + // With only 16 slots (24 KB ring), a full-window burst from the peer 54 + // would overflow the hardware ring between NetPoll calls (typically 55 + // every 10 ms). 64 slots = 96 KB ring gives headroom beyond TCP_WND 56 + // so NetPoll frequency isn't the binding constraint. 57 + // 58 + // PCnet requires RX/TX ring sizes to be a power of two. The init 59 + // block's tlen_rlen field stores log2 of the count, so 64 → RX_LOG2=6. 60 + // Update both NUM_RX and the init_block shift below together. 61 + #define NUM_RX 64 62 + #define NUM_TX 8 63 + #define BUF_SZ 1536 64 + #define DESC_OWN 0x8000 65 + #define DESC_ERR 0x4000 66 + #define DESC_STP 0x0200 67 + #define DESC_ENP 0x0100 68 + 69 + // Descriptor access helpers (raw byte offsets into descriptor ring) 70 + U32 *DescBase(U8 *d, I64 i) { return d + i * 16; } 71 + I16 *DescBufLen(U8 *d, I64 i) { return d + i * 16 + 4; } 72 + U16 *DescStatus(U8 *d, I64 i) { return d + i * 16 + 6; } 73 + U32 *DescMsgLen(U8 *d, I64 i) { return d + i * 16 + 8; } 74 + 75 + U8 *rx_desc; // NUM_RX * 16 bytes, 16-byte aligned 76 + U8 *tx_desc; // NUM_TX * 16 bytes, 16-byte aligned 77 + U8 *rx_bufs; // NUM_RX * BUF_SZ 78 + U8 *tx_bufs; // NUM_TX * BUF_SZ 79 + I64 rx_idx; 80 + I64 tx_idx; 81 + U8 nic_mac[6]; 82 + 83 + // Init block (28 bytes, 4-byte aligned) 84 + // [0:1] mode 85 + // [2:3] tlen_rlen (TX log2 << 12 | RX log2 << 4) 86 + // [4:9] mac 87 + // [10:11] reserved 88 + // [12:19] logical address filter 89 + // [20:23] rx_ring physical address 90 + // [24:27] tx_ring physical address 91 + 92 + U8 *init_block; 93 + 94 + // ---- Init ---- 95 + 96 + I64 NicInit() 97 + { 98 + I64 i; 99 + 100 + nic_io_base = PcnetFindDevice; 101 + if (!nic_io_base) { "Nic: not found!\n"; return -1; } 102 + 103 + PcnetReset; 104 + 105 + // Set SWSTYLE=2 (32-bit) via BCR20 106 + // Read current BCR20, clear low byte, set to 2 107 + U16 bcr20 = PcnetReadBCR(20); 108 + bcr20 = (bcr20 & 0xFF00) | 2; 109 + PcnetWriteBCR(20, bcr20); 110 + 111 + // Read MAC from APROM 112 + for (i = 0; i < 6; i++) 113 + nic_mac[i] = InU8(nic_io_base + i); 114 + if (!elf_quiet) 115 + "Nic: MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", 116 + nic_mac[0], nic_mac[1], nic_mac[2], nic_mac[3], nic_mac[4], nic_mac[5]; 117 + 118 + // Allocate descriptor rings (16-byte aligned) and buffers 119 + rx_desc = CAllocAligned(NUM_RX * 16, 16); 120 + tx_desc = CAllocAligned(NUM_TX * 16, 16); 121 + rx_bufs = CAlloc(NUM_RX * BUF_SZ); 122 + tx_bufs = CAlloc(NUM_TX * BUF_SZ); 123 + rx_idx = 0; 124 + tx_idx = 0; 125 + 126 + // Init RX descriptors 127 + for (i = 0; i < NUM_RX; i++) { 128 + *DescBase(rx_desc, i) = rx_bufs + i * BUF_SZ; 129 + *DescBufLen(rx_desc, i) = -BUF_SZ; // two's complement, 16-bit 130 + *DescStatus(rx_desc, i) = DESC_OWN; // card owns it 131 + *DescMsgLen(rx_desc, i) = 0; 132 + } 133 + 134 + // Init TX descriptors (host owns, empty) 135 + for (i = 0; i < NUM_TX; i++) { 136 + *DescBase(tx_desc, i) = tx_bufs + i * BUF_SZ; 137 + *DescBufLen(tx_desc, i) = 0; 138 + *DescStatus(tx_desc, i) = 0; 139 + *DescMsgLen(tx_desc, i) = 0; 140 + } 141 + 142 + // Build init block (28 bytes) 143 + init_block = CAllocAligned(28, 4); 144 + *(init_block(U16 *)) = 0; // mode = 0 (normal) 145 + // tlen_rlen: TX_LOG2=3 (8 entries) << 12 | RX_LOG2=6 (64 entries) << 4 146 + // NUM_RX above MUST match 2^RX_LOG2. Bumped from 4 to 6 to match 147 + // the larger TCP window — see comment on NUM_RX. 148 + *((init_block + 2)(U16 *)) = (3 << 12) | (6 << 4); 149 + MemCpy(init_block + 4, nic_mac, 6); // MAC address 150 + *((init_block + 10)(U16 *)) = 0; // reserved 151 + // Logical address filter = accept all 152 + *((init_block + 12)(U32 *)) = 0xFFFFFFFF; 153 + *((init_block + 16)(U32 *)) = 0xFFFFFFFF; 154 + // Ring addresses 155 + *((init_block + 20)(U32 *)) = rx_desc; 156 + *((init_block + 24)(U32 *)) = tx_desc; 157 + 158 + // Stop card 159 + PcnetWriteCSR(0, 0x0004); 160 + 161 + // Set init block address (CSR1=low16, CSR2=high16) 162 + U32 ib = init_block; 163 + PcnetWriteCSR(1, ib & 0xFFFF); 164 + PcnetWriteCSR(2, (ib >> 16) & 0xFFFF); 165 + 166 + // Init 167 + PcnetWriteCSR(0, 0x0001); 168 + for (i = 0; i < 1000000; i++) 169 + if (PcnetReadCSR(0) & 0x0100) break; 170 + if (!(PcnetReadCSR(0) & 0x0100)) { "Nic: init timeout\n"; return -1; } 171 + 172 + // Start + enable interrupts 173 + PcnetWriteCSR(0, 0x0042); 174 + if (!elf_quiet) "Nic: initialized, CSR0=0x%04X\n", PcnetReadCSR(0); 175 + 176 + return 0; 177 + } 178 + 179 + // ---- Send ---- 180 + 181 + I64 NicSend(U8 *frame, I64 len) 182 + { 183 + if (len > BUF_SZ || len < 14) return -1; 184 + I64 idx = tx_idx; 185 + 186 + // Check OWN bit — if set, card still owns it 187 + if (*DescStatus(tx_desc, idx) & DESC_OWN) return -1; 188 + 189 + MemCpy(tx_bufs + idx * BUF_SZ, frame, len); 190 + *DescBufLen(tx_desc, idx) = -len; // two's complement, 16-bit 191 + *DescMsgLen(tx_desc, idx) = 0; 192 + // Set OWN + STP + ENP (single packet, start and end) 193 + *DescStatus(tx_desc, idx) = DESC_OWN | DESC_STP | DESC_ENP; 194 + 195 + tx_idx = (idx + 1) % NUM_TX; 196 + 197 + // Trigger transmit 198 + PcnetWriteCSR(0, 0x0048); 199 + return len; 200 + } 201 + 202 + // ---- Receive ---- 203 + 204 + I64 NicRecv(U8 *buf, I64 maxlen) 205 + { 206 + I64 idx = rx_idx; 207 + 208 + // OWN set = card still owns it = no data 209 + if (*DescStatus(rx_desc, idx) & DESC_OWN) return 0; 210 + 211 + // Check for errors 212 + if (*DescStatus(rx_desc, idx) & DESC_ERR) { 213 + *DescBufLen(rx_desc, idx) = -BUF_SZ; 214 + *DescStatus(rx_desc, idx) = DESC_OWN; 215 + rx_idx = (idx + 1) % NUM_RX; 216 + return 0; 217 + } 218 + 219 + I64 len = *DescMsgLen(rx_desc, idx) & 0xFFFF; 220 + if (len > maxlen) len = maxlen; 221 + MemCpy(buf, rx_bufs + idx * BUF_SZ, len); 222 + 223 + // Give back to card 224 + *DescBufLen(rx_desc, idx) = -BUF_SZ; 225 + *DescStatus(rx_desc, idx) = DESC_OWN; 226 + *DescMsgLen(rx_desc, idx) = 0; 227 + rx_idx = (idx + 1) % NUM_RX; 228 + 229 + return len; 230 + } 231 + 232 + // ---- C-callable wrappers ---- 233 + 234 + // C-callable wrappers: save HolyC regs, push C args for HolyC call, restore 235 + // C-callable NIC wrappers — built as machine code at runtime 236 + // to avoid HolyC prologue/epilogue clobbering C callee-saved regs. 237 + U8 *_NIC_SEND_C; 238 + U8 *_NIC_RECV_C; 239 + 240 + U0 NicBuildTrampolines() 241 + { 242 + // PUSH RBX; PUSH RSI; PUSH RDI; MOV RAX,imm64; CALL RAX; POP RBX; RET 243 + // RBX is in HolyC's CLOBBERED set but C's callee-saved set. 244 + // NicSend/NicRecv do RET1 16 which pops RSI+RDI pushes. 245 + U64 addr; 246 + I64 i; 247 + 248 + _NIC_SEND_C = MAlloc(32, Fs->code_heap); 249 + i = 0; 250 + _NIC_SEND_C[i++] = 0x53; // PUSH RBX 251 + _NIC_SEND_C[i++] = 0x56; // PUSH RSI 252 + _NIC_SEND_C[i++] = 0x57; // PUSH RDI 253 + _NIC_SEND_C[i++] = 0x48; _NIC_SEND_C[i++] = 0xB8; // MOV RAX, imm64 254 + addr = &NicSend; 255 + MemCpy(_NIC_SEND_C + i, &addr, 8); i += 8; 256 + _NIC_SEND_C[i++] = 0xFF; _NIC_SEND_C[i++] = 0xD0; // CALL RAX 257 + _NIC_SEND_C[i++] = 0x5B; // POP RBX 258 + _NIC_SEND_C[i++] = 0xC3; // RET 259 + 260 + _NIC_RECV_C = MAlloc(32, Fs->code_heap); 261 + i = 0; 262 + _NIC_RECV_C[i++] = 0x53; // PUSH RBX 263 + _NIC_RECV_C[i++] = 0x56; // PUSH RSI 264 + _NIC_RECV_C[i++] = 0x57; // PUSH RDI 265 + _NIC_RECV_C[i++] = 0x48; _NIC_RECV_C[i++] = 0xB8; // MOV RAX, imm64 266 + addr = &NicRecv; 267 + MemCpy(_NIC_RECV_C + i, &addr, 8); i += 8; 268 + _NIC_RECV_C[i++] = 0xFF; _NIC_RECV_C[i++] = 0xD0; // CALL RAX 269 + _NIC_RECV_C[i++] = 0x5B; // POP RBX 270 + _NIC_RECV_C[i++] = 0xC3; // RET 271 + 272 + if (!elf_quiet) "Nic: trampolines at 0x%X 0x%X\n", _NIC_SEND_C, _NIC_RECV_C; 273 + } 274 + 275 + if (!elf_quiet) "Nic.HC: PCnet driver ready\n"; 276 + #endif // NIC_HC
+84
holyc/vodbrowser/lib/TosCallbacks.HC
··· 1 + #ifndef TOSCALLBACKS_HC 2 + #define TOSCALLBACKS_HC 3 + // TosCallbacks.HC - C-callable TempleOS utility wrappers 4 + // 5 + // These are noargpop functions callable from C code via function pointers. 6 + // C passes args in RDI/RSI (SysV ABI), these wrappers bridge to HolyC. 7 + // 8 + // All wrappers save/restore RBX around HolyC calls because RBX is in 9 + // HolyC's CLOBBERED set (freely trashed) but C's callee-saved set. 10 + // See NOTES.md for the full ABI analysis. 11 + 12 + U64 _TOS_RAND() noargpop 13 + { 14 + I64 noreg result; 15 + asm { 16 + PUSH RBX 17 + CALL &RandU64 // 0-arg, plain RET 18 + MOV U64 &result[RBP], RAX 19 + POP RBX 20 + }; 21 + return result; 22 + } 23 + 24 + // Inner function does the actual tick computation. 25 + // _TOS_TICKS wraps it with PUSH/POP RBX in a single asm block. 26 + U64 _TOS_TICKS_INNER() noargpop 27 + { 28 + return cnts.jiffies * 1000 / JIFFY_FREQ; 29 + } 30 + 31 + U64 _TOS_TICKS() noargpop 32 + { 33 + I64 noreg result; 34 + asm { 35 + PUSH RBX 36 + CALL &_TOS_TICKS_INNER // noargpop, plain RET 37 + MOV U64 &result[RBP], RAX 38 + POP RBX 39 + }; 40 + return result; 41 + } 42 + 43 + U64 _TOS_MALLOC() noargpop 44 + { 45 + // C passes size in RDI. MAlloc(size, NULL) uses RET1 16. 46 + I64 noreg result; 47 + asm { 48 + PUSH RBX 49 + PUSH 0 // mem_task = NULL 50 + PUSH RDI // size 51 + CALL &MAlloc // RET1 16 52 + MOV U64 &result[RBP], RAX 53 + POP RBX 54 + }; 55 + return result; 56 + } 57 + 58 + U64 _TOS_FREE() noargpop 59 + { 60 + // C passes ptr in RDI. Free(ptr) uses RET1 8. 61 + asm { 62 + PUSH RBX 63 + PUSH RDI 64 + CALL &Free // RET1 8 65 + POP RBX 66 + }; 67 + return 0; 68 + } 69 + 70 + // Sleep(ms) callback for C code — uses same trampoline pattern as 71 + // NicSend/NicRecv which work fine from the big stack. 72 + // C passes ms in RDI. Sleep(ms) uses RET1 8. 73 + U64 _TOS_SLEEP() noargpop 74 + { 75 + asm { 76 + PUSH RBX 77 + PUSH RDI // ms arg for Sleep 78 + CALL &Sleep // RET1 8 79 + POP RBX 80 + }; 81 + return 0; 82 + } 83 + 84 + #endif // TOSCALLBACKS_HC
+3
holyc/vodbrowser/lib/Version.HC
··· 1 + // Auto-generated by deploy.sh 2 + U0 Ver() { "VodBrowser build: 2026-04-11 17:39:34\n"; } 3 + Ver;
isos/.gitkeep

This is a binary file and will not be displayed.

+134
media/Makefile
··· 1 + ROOT = .. 2 + ELF_DIR = $(ROOT)/elf 3 + 4 + # Libraries 5 + LIBMOV_DIR = $(ROOT)/lib/media-server/libmov 6 + OPENH264_DIR = $(ROOT)/lib/openh264 7 + 8 + # Cross-compiler for the TempleOS ELF (x86_64-linux-musl target, 9 + # freestanding, no SSE/MMX/float — TempleOS V5.03 has no CR4.OSFXSR). 10 + ZIG = zig 11 + ZIGCC = $(ZIG) cc -target x86_64-linux-musl 12 + ZIGCXX = $(ZIG) c++ -target x86_64-linux-musl 13 + LLD = $(ZIG) ld.lld 14 + XCFLAGS = -fPIC -fno-stack-protector -fno-sanitize=all -fwrapv -O3 \ 15 + -fomit-frame-pointer -funroll-loops \ 16 + -mno-sse -mno-sse2 -mno-mmx -msoft-float -mno-red-zone 17 + XCXXFLAGS = $(XCFLAGS) -fno-exceptions -fno-rtti -std=c++11 \ 18 + -DVODBROWSER_NO_FLOAT -DVODBROWSER_NO_DEBLOCK 19 + 20 + # Include paths 21 + INCLUDES = -I. \ 22 + -I$(LIBMOV_DIR)/include \ 23 + -I$(LIBMOV_DIR)/source \ 24 + -I$(OPENH264_DIR)/codec/api \ 25 + -I$(OPENH264_DIR)/codec/api/wels \ 26 + -I$(OPENH264_DIR)/codec/common/inc \ 27 + -I$(OPENH264_DIR)/codec/decoder/core/inc \ 28 + -I$(OPENH264_DIR)/codec/decoder/plus/inc 29 + 30 + # ---- Source file lists ---- 31 + 32 + LIBMOV_SRCS = $(wildcard $(LIBMOV_DIR)/source/*.c) 33 + 34 + # openh264 decoder (C++) — no ASM, no encoder 35 + OH264_DECODER_SRCS = $(wildcard $(OPENH264_DIR)/codec/decoder/core/src/*.cpp) \ 36 + $(OPENH264_DIR)/codec/decoder/plus/src/welsDecoderExt.cpp 37 + OH264_COMMON_SRCS = $(wildcard $(OPENH264_DIR)/codec/common/src/*.cpp) 38 + 39 + .PHONY: all elf clean 40 + 41 + all: elf 42 + 43 + 44 + XBUILD = build_elf 45 + XOBJS = $(XBUILD)/media_api.o $(XBUILD)/media_entry.o $(XBUILD)/media_minilib.o 46 + 47 + # libmov objects (C) 48 + XLIBMOV_OBJS = $(patsubst $(LIBMOV_DIR)/source/%.c,$(XBUILD)/libmov_%.o,$(LIBMOV_SRCS)) 49 + 50 + # openh264 objects (C++) — NO X86_ASM defined, so C fallbacks are used 51 + XOH264_DEC_OBJS = $(patsubst $(OPENH264_DIR)/codec/decoder/core/src/%.cpp,$(XBUILD)/oh264_dec_%.o,$(wildcard $(OPENH264_DIR)/codec/decoder/core/src/*.cpp)) \ 52 + $(XBUILD)/oh264_dec_welsDecoderExt.o 53 + XOH264_COM_OBJS = $(patsubst $(OPENH264_DIR)/codec/common/src/%.cpp,$(XBUILD)/oh264_com_%.o,$(OH264_COMMON_SRCS)) 54 + 55 + elf: Media.prg 56 + 57 + # ---- Apply patches to submodule sources (not committed upstream) ---- 58 + # openh264: removes float/SSE usage that is incompatible with TempleOS. 59 + # libmov: adds mov_reader_get_track_timescale() used by media_api.c's 60 + # PTS pacing path. Both patches are idempotent — the --check --reverse 61 + # test fails with a clean tree, succeeds with an already-patched tree. 62 + $(XBUILD)/.oh264_patched: openh264-no-float.patch | $(XBUILD) 63 + @cd $(OPENH264_DIR) && \ 64 + if git apply --check --reverse ../../media/openh264-no-float.patch 2>/dev/null; then \ 65 + echo "openh264: patch already applied"; \ 66 + else \ 67 + echo "openh264: applying no-float patch"; \ 68 + git apply ../../media/openh264-no-float.patch; \ 69 + fi 70 + @touch $@ 71 + 72 + LIBMOV_ROOT = $(ROOT)/lib/media-server 73 + $(XBUILD)/.libmov_patched: libmov-timescale.patch | $(XBUILD) 74 + @cd $(LIBMOV_ROOT) && \ 75 + if git apply --check --reverse ../../media/libmov-timescale.patch 2>/dev/null; then \ 76 + echo "libmov: patch already applied"; \ 77 + else \ 78 + echo "libmov: applying timescale patch"; \ 79 + git apply ../../media/libmov-timescale.patch; \ 80 + fi 81 + @touch $@ 82 + 83 + # Our C sources. 84 + $(XBUILD)/media_api.o: media_api.c media_api.h | $(XBUILD) 85 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 86 + 87 + $(XBUILD)/media_entry.o: media_entry.c media_api.h | $(XBUILD) 88 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 89 + 90 + $(XBUILD)/media_minilib.o: media_minilib.c | $(XBUILD) 91 + $(ZIGCC) $(XCFLAGS) -c -o $@ $< 92 + 93 + # libmov (C) — depends on the libmov timescale patch 94 + $(XBUILD)/libmov_%.o: $(LIBMOV_DIR)/source/%.c $(XBUILD)/.libmov_patched | $(XBUILD) 95 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -DMOV_READER_FMP4_FAST=1 -c -o $@ $< 96 + 97 + # openh264 decoder core (C++) — depends on patch 98 + $(XBUILD)/oh264_dec_%.o: $(OPENH264_DIR)/codec/decoder/core/src/%.cpp $(XBUILD)/.oh264_patched | $(XBUILD) 99 + $(ZIGCXX) $(XCXXFLAGS) $(INCLUDES) -c -o $@ $< 100 + 101 + # openh264 decoder plus (C++) 102 + $(XBUILD)/oh264_dec_welsDecoderExt.o: $(OPENH264_DIR)/codec/decoder/plus/src/welsDecoderExt.cpp $(XBUILD)/.oh264_patched | $(XBUILD) 103 + $(ZIGCXX) $(XCXXFLAGS) $(INCLUDES) -c -o $@ $< 104 + 105 + # openh264 common (C++) 106 + $(XBUILD)/oh264_com_%.o: $(OPENH264_DIR)/codec/common/src/%.cpp $(XBUILD)/.oh264_patched | $(XBUILD) 107 + $(ZIGCXX) $(XCXXFLAGS) $(INCLUDES) -c -o $@ $< 108 + 109 + # Setup.s (ELF entry) 110 + $(XBUILD)/setup.o: $(ELF_DIR)/setup.s | $(XBUILD) 111 + $(ZIGCC) -c -fPIC -fno-stack-protector -nostdlib -o $@ $< 112 + 113 + # Link 114 + $(XBUILD)/unoffset.elf: $(XBUILD)/setup.o $(XOBJS) $(XLIBMOV_OBJS) $(XOH264_DEC_OBJS) $(XOH264_COM_OBJS) 115 + $(LLD) --no-relax -T $(ELF_DIR)/unoffset.x -o $@ $^ 116 + 117 + $(XBUILD)/offset.elf: $(XBUILD)/setup.o $(XOBJS) $(XLIBMOV_OBJS) $(XOH264_DEC_OBJS) $(XOH264_COM_OBJS) 118 + $(LLD) --no-relax -T $(ELF_DIR)/offset.x -o $@ $^ 119 + 120 + $(XBUILD)/unoffset.bin: $(XBUILD)/unoffset.elf 121 + python3 $(ELF_DIR)/elf2bin.py $< $@ 122 + 123 + $(XBUILD)/offset.bin: $(XBUILD)/offset.elf 124 + python3 $(ELF_DIR)/elf2bin.py $< $@ 125 + 126 + Media.prg: $(XBUILD)/unoffset.bin $(XBUILD)/offset.bin 127 + cd $(XBUILD) && python3 ../../elf/make_program.py ../../media/Media.prg 128 + 129 + $(XBUILD): 130 + mkdir -p $(XBUILD) 131 + 132 + clean: 133 + rm -rf $(XBUILD) Media.prg 134 + @cd $(OPENH264_DIR) && git checkout -- . 2>/dev/null || true
+31
media/libmov-timescale.patch
··· 1 + diff --git a/libmov/include/mov-reader.h b/libmov/include/mov-reader.h 2 + index 0fbcb7a..d10737e 100644 3 + --- a/libmov/include/mov-reader.h 4 + +++ b/libmov/include/mov-reader.h 5 + @@ -27,6 +27,9 @@ int mov_reader_getinfo(mov_reader_t* mov, struct mov_reader_trackinfo_t *ontrack 6 + 7 + uint64_t mov_reader_getduration(mov_reader_t* mov); 8 + 9 + +/// @return track mdhd timescale (e.g. 60000), or 0 if track not found. 10 + +uint32_t mov_reader_get_track_timescale(mov_reader_t* mov, uint32_t track); 11 + + 12 + /// audio: AAC raw data, don't include ADTS/AudioSpecificConfig 13 + /// video: 4-byte data length(don't include self length) + H.264 NALU(don't include 0x00000001) 14 + /// @param[in] flags MOV_AV_FLAG_xxx, such as: MOV_AV_FLAG_KEYFREAME 15 + diff --git a/libmov/source/mov-reader.c b/libmov/source/mov-reader.c 16 + index b558436..a892f79 100755 17 + --- a/libmov/source/mov-reader.c 18 + +++ b/libmov/source/mov-reader.c 19 + @@ -600,6 +600,12 @@ uint64_t mov_reader_getduration(struct mov_reader_t* reader) 20 + return 0 != reader->mov.mvhd.timescale ? reader->mov.mvhd.duration * 1000 / reader->mov.mvhd.timescale : 0; 21 + } 22 + 23 + +uint32_t mov_reader_get_track_timescale(struct mov_reader_t* reader, uint32_t track) 24 + +{ 25 + + struct mov_track_t* t = mov_find_track(&reader->mov, track); 26 + + return t ? t->mdhd.timescale : 0; 27 + +} 28 + + 29 + #define DIFF(a, b) ((a) > (b) ? ((a) - (b)) : ((b) - (a))) 30 + 31 + static int mov_stss_seek(struct mov_track_t* track, int64_t *timestamp)
+937
media/media_api.c
··· 1 + /* 2 + * media_api.c — VodBrowser media library implementation 3 + * 4 + * Wraps libmov + openh264 into a streaming decode API. 5 + */ 6 + #include "media_api.h" 7 + 8 + #include <stdio.h> 9 + #include <stdlib.h> 10 + #include <string.h> 11 + 12 + #include "mov-reader.h" 13 + #include "mov-format.h" 14 + #include "wels/codec_api.h" 15 + 16 + /* Full-width wallclock accessor from media_minilib. Returns 0 if 17 + * the ticks callback wasn't plumbed in (safe fallback: dynamic skip 18 + * sees lead == target and won't trigger). */ 19 + extern uint64_t media_ticks_ms(void); 20 + 21 + /* ---- Memory buffer I/O for libmov ---- */ 22 + 23 + typedef struct { 24 + const unsigned char *data; 25 + size_t len; 26 + int64_t pos; 27 + } membuf_t; 28 + 29 + static int membuf_read(void *p, void *d, uint64_t b) { 30 + membuf_t *m = (membuf_t *)p; 31 + if (m->pos + (int64_t)b > (int64_t)m->len) return -1; 32 + memcpy(d, m->data + m->pos, (size_t)b); 33 + m->pos += (int64_t)b; 34 + return 0; 35 + } 36 + static int membuf_write(void *p, const void *d, uint64_t b) { 37 + (void)p; (void)d; (void)b; return -1; 38 + } 39 + static int membuf_seek(void *p, int64_t off) { 40 + membuf_t *m = (membuf_t *)p; 41 + if (off < 0) off = (int64_t)m->len + off; 42 + if (off < 0 || off > (int64_t)m->len) return -1; 43 + m->pos = off; 44 + return 0; 45 + } 46 + static int64_t membuf_tell(void *p) { return ((membuf_t *)p)->pos; } 47 + 48 + static const struct mov_buffer_t membuf_vtable = { 49 + membuf_read, membuf_write, membuf_seek, membuf_tell 50 + }; 51 + 52 + /* ---- YUV to RGB ---- */ 53 + 54 + static inline unsigned char clamp8(int v) { 55 + return v < 0 ? 0 : (v > 255 ? 255 : (unsigned char)v); 56 + } 57 + 58 + static void yuv420_to_rgb(const unsigned char *y, const unsigned char *u, 59 + const unsigned char *v, 60 + int ys, int us, int vs, 61 + int w, int h, unsigned char *rgb) 62 + { 63 + for (int row = 0; row < h; row++) { 64 + for (int col = 0; col < w; col++) { 65 + int yv = y[row * ys + col]; 66 + int uv = u[(row / 2) * us + (col / 2)] - 128; 67 + int vv = v[(row / 2) * vs + (col / 2)] - 128; 68 + unsigned char *px = rgb + (row * w + col) * 3; 69 + px[0] = clamp8(yv + ((359 * vv) >> 8)); 70 + px[1] = clamp8(yv - ((88 * uv + 183 * vv) >> 8)); 71 + px[2] = clamp8(yv + ((454 * uv) >> 8)); 72 + } 73 + } 74 + } 75 + 76 + /* ---- RGB→palette LUT + fused YUV→palette LUT (32 KB each, built once) ---- 77 + * 78 + * rgb_to_pal_lut[] is used by the Floyd-Steinberg dither path, which 79 + * needs to propagate quantisation error in RGB space (so it has to 80 + * compute R, G, B explicitly before looking up the nearest palette 81 + * entry). 82 + * 83 + * yuv_to_pal_lut[] collapses the full YUV→RGB→nearest-color chain into 84 + * a single 32 KB table lookup, with the chroma boost and YUV→RGB 85 + * matrix already baked in. The NEAREST dither path uses it to replace 86 + * ~11 arithmetic ops per pixel with one load. Same index layout as 87 + * rgb_to_pal_lut: (y5 << 10) | (u5 << 5) | v5 where each component 88 + * has been quantised from 8-bit to 5-bit. 89 + */ 90 + static unsigned char rgb_to_pal_lut[32 * 32 * 32]; 91 + static unsigned char yuv_to_pal_lut[32 * 32 * 32]; 92 + static int lut_built = 0; 93 + 94 + static const unsigned char tos_r[16] = { 0, 0, 0, 0, 170, 170, 170, 170, 95 + 85, 85, 85, 85, 255, 255, 255, 255}; 96 + static const unsigned char tos_g[16] = { 0, 0, 170, 170, 0, 0, 85, 170, 97 + 85, 85, 255, 255, 85, 85, 255, 255}; 98 + static const unsigned char tos_b[16] = { 0, 170, 0, 170, 0, 170, 0, 170, 99 + 85, 255, 85, 255, 85, 255, 85, 255}; 100 + 101 + static inline int clamp255_(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } 102 + 103 + static void build_pal_lut(void) { 104 + if (lut_built) return; 105 + 106 + /* ---- Pass 1: rgb_to_pal_lut (same logic as before) ---- */ 107 + for (int ri = 0; ri < 32; ri++) 108 + for (int gi = 0; gi < 32; gi++) 109 + for (int bi = 0; bi < 32; bi++) { 110 + int r = ri * 255 / 31, g = gi * 255 / 31, b = bi * 255 / 31; 111 + 112 + /* Pixel saturation: how chromatic is this color? */ 113 + int mx = r, mn = r; 114 + if (g > mx) mx = g; if (g < mn) mn = g; 115 + if (b > mx) mx = b; if (b < mn) mn = b; 116 + int sat = mx - mn; 117 + 118 + int best = 0, best_d = 0x7FFFFFFF; 119 + for (int i = 0; i < 16; i++) { 120 + int dr = r - tos_r[i]; 121 + int dg = g - tos_g[i]; 122 + int db = b - tos_b[i]; 123 + int d = dr*dr*3 + dg*dg*4 + db*db*2; 124 + /* Penalize grays (0=BLACK,7=LTGRAY,8=DKGRAY,15=WHITE) 125 + * when pixel has chroma. Prevents grays from absorbing 126 + * muted colors that should map to colored entries. */ 127 + if ((i == 0 || i == 7 || i == 8 || i == 15) && sat > 30) 128 + d += sat * sat * 4; 129 + if (d < best_d) { best_d = d; best = i; } 130 + } 131 + rgb_to_pal_lut[(ri << 10) | (gi << 5) | bi] = (unsigned char)best; 132 + } 133 + 134 + /* ---- Pass 2: yuv_to_pal_lut ---- 135 + * 136 + * Same 5-5-5 quantisation on each YUV component. For each entry we 137 + * reconstruct the approximate 8-bit Y/U/V, apply the same chroma 138 + * boost the dither hot path uses (uv *= 1.5, vv *= 3), run the 139 + * YUV→RGB matrix, then look up the result in rgb_to_pal_lut. The 140 + * per-pixel NEAREST inner loop then collapses to a single indexed 141 + * load. */ 142 + for (int yi = 0; yi < 32; yi++) 143 + for (int ui = 0; ui < 32; ui++) 144 + for (int vi = 0; vi < 32; vi++) { 145 + int yv = yi * 255 / 31; 146 + int uv = (ui * 255 / 31) - 128; 147 + int vv = (vi * 255 / 31) - 128; 148 + 149 + /* Chroma boost — mirror the dither_frame inner loop. */ 150 + uv = (uv * 3) >> 1; 151 + vv = vv * 3; 152 + 153 + int r = clamp255_(yv + ((359 * vv) >> 8)); 154 + int g = clamp255_(yv - ((88 * uv + 183 * vv) >> 8)); 155 + int b = clamp255_(yv + ((454 * uv) >> 8)); 156 + 157 + yuv_to_pal_lut[(yi << 10) | (ui << 5) | vi] = 158 + rgb_to_pal_lut[((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)]; 159 + } 160 + 161 + lut_built = 1; 162 + } 163 + 164 + /* ---- Dither mode (user-selectable at runtime) ---- */ 165 + 166 + static int g_dither_mode = MEDIA_DITHER_FLOYD; 167 + 168 + void media_set_dither_mode(int mode) { 169 + if (mode < 0 || mode > 2) return; 170 + g_dither_mode = mode; 171 + } 172 + 173 + /* ---- Dynamic non-reference frame skip ---- 174 + * 175 + * When g_skip_non_ref is ON (master switch set from the HolyC pref), 176 + * on_frame compares each frame's PTS against wallclock and drops 177 + * non-reference H.264 slices (nal_ref_idc == 0) only when we're 178 + * actually falling behind. This is the main lever for bringing 179 + * playback time in line with source duration when decode is the 180 + * bottleneck — modern streaming encodes use hierarchical B frames 181 + * heavily and 25-50% of frames are typically non-reference. 182 + * 183 + * The threshold is derived from the observed source frame interval 184 + * so it adapts to 60/30/24/VFR sources without tuning. We trigger 185 + * a skip when we're more than DEC_SKIP_FRAME_MULT source-frame 186 + * intervals late. If we don't yet have PTS history (first frame 187 + * of a segment) or the computed interval is out of a sane range, 188 + * we fall back to DEC_SKIP_THRESHOLD_FALLBACK_MS. */ 189 + #define DEC_SKIP_THRESHOLD_FALLBACK_MS 50 /* used when adaptive unavailable */ 190 + #define DEC_SKIP_FRAME_MULT 2 /* threshold = mult * frame interval */ 191 + #define DEC_SKIP_INTERVAL_MIN_MS 5 /* sane range for adaptive calc */ 192 + #define DEC_SKIP_INTERVAL_MAX_MS 200 193 + 194 + static int g_skip_non_ref = 0; 195 + 196 + void media_set_skip_non_ref(int enabled) { 197 + g_skip_non_ref = enabled ? 1 : 0; 198 + } 199 + 200 + /* Bayer threshold table kept around for media_get_pal's legacy non-fused 201 + * path; the fused path (media_next_pal) uses dither_frame() below. */ 202 + static const signed char bayer_thr[16] = { 203 + -32, 0, -24, 8, 16, -16, 24, -8, 204 + -20, 12, -28, 4, 28, -4, 20, -12 205 + }; 206 + 207 + static inline int clamp255(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } 208 + 209 + /* ---- Persistent dither scratch buffers ---- 210 + * 211 + * Hoisted to module-level so the Floyd hot path doesn't hit the 212 + * allocator once per frame. These are owned by Media.prg for its 213 + * whole lifetime — grow-only, never freed (the buffer sizes are 214 + * bounded by max target resolution, ~a few KB each, and Media.prg 215 + * is loaded once per Browse session anyway). 216 + * 217 + * g_err_cur / g_err_nxt : Floyd-Steinberg error rings. Capacity 218 + * in shorts is (max_dw + 2) * 3. 219 + * g_sc_table : precomputed source-column lookup 220 + * `col * sw / dw` for the current frame, 221 + * turning a per-pixel mul+div into a 222 + * single memory load. Rebuilt only when 223 + * dw or sw changes. 224 + */ 225 + static short *g_err_cur; 226 + static short *g_err_nxt; 227 + static int g_err_cap; /* allocated capacity in shorts */ 228 + 229 + static int *g_sc_table; 230 + static int g_sc_table_cap; /* allocated capacity in entries */ 231 + static int g_sc_table_dw; 232 + static int g_sc_table_sw; 233 + 234 + static int ensure_err_buffers(int bufsz) { 235 + if (g_err_cap >= bufsz) return 1; 236 + free(g_err_cur); free(g_err_nxt); 237 + g_err_cur = (short *)malloc(bufsz * sizeof(short)); 238 + g_err_nxt = (short *)malloc(bufsz * sizeof(short)); 239 + if (!g_err_cur || !g_err_nxt) { 240 + free(g_err_cur); g_err_cur = NULL; 241 + free(g_err_nxt); g_err_nxt = NULL; 242 + g_err_cap = 0; 243 + return 0; 244 + } 245 + g_err_cap = bufsz; 246 + return 1; 247 + } 248 + 249 + static int ensure_sc_table(int dw, int sw) { 250 + /* Already valid for this (dw, sw) pair? */ 251 + if (g_sc_table && g_sc_table_dw == dw && g_sc_table_sw == sw) return 1; 252 + if (g_sc_table_cap < dw) { 253 + free(g_sc_table); 254 + g_sc_table = (int *)malloc(dw * sizeof(int)); 255 + if (!g_sc_table) { 256 + g_sc_table_cap = 0; 257 + g_sc_table_dw = g_sc_table_sw = 0; 258 + return 0; 259 + } 260 + g_sc_table_cap = dw; 261 + } 262 + for (int col = 0; col < dw; col++) { 263 + g_sc_table[col] = col * sw / dw; 264 + } 265 + g_sc_table_dw = dw; 266 + g_sc_table_sw = sw; 267 + return 1; 268 + } 269 + 270 + /* ---- Unified YUV→palette dither with optional downscale ---- 271 + * 272 + * Single entry point for the fused path. Handles both same-size 273 + * (sw==dw, sh==dh) and downscaled (sw>dw, sh>dh) output and dispatches 274 + * on g_dither_mode at the outer level so per-pixel hot loops stay 275 + * branch-free. 276 + * 277 + * Hot-path optimisations applied here: 278 + * - Error rings hoisted to statics; no per-frame calloc/free. 279 + * - Source-column sample positions precomputed into sc_table once 280 + * per frame; the inner loop does a load instead of `col * sw / dw`. 281 + * - Per-row Y/U/V row pointers hoisted out of the column loop. 282 + * - Floyd right-neighbor writes are unconditional: the buffer is 283 + * already padded to (dw + 2) * 3 shorts so writes at the last 284 + * column land in a dead-padding slot that memset wipes before 285 + * the next row. One fewer branch mispredict per pixel. 286 + * 287 + * Modes: 288 + * FLOYD — Floyd-Steinberg error diffusion with saturation-aware LUT. 289 + * NEAREST — plain nearest-color LUT lookup, no error diffusion. 290 + * GRAY — luma plane only, quantised into 4 TempleOS palette grays. 291 + */ 292 + static void dither_frame( 293 + const unsigned char *yp, const unsigned char *up, const unsigned char *vp, 294 + int ys, int us, int vs, 295 + int sw, int sh, 296 + unsigned char *pal, int dw, int dh) 297 + { 298 + int mode = g_dither_mode; 299 + 300 + if (!ensure_sc_table(dw, sw)) return; 301 + const int *sc_table = g_sc_table; 302 + 303 + if (mode == MEDIA_DITHER_GRAY) { 304 + for (int row = 0; row < dh; row++) { 305 + int sr = row * sh / dh; 306 + const unsigned char *yrow = yp + sr * ys; 307 + unsigned char *out = pal + row * dw; 308 + for (int col = 0; col < dw; col++) { 309 + int yv = yrow[sc_table[col]]; 310 + int idx; 311 + if (yv < 64) idx = 0; /* BLACK */ 312 + else if (yv < 128) idx = 8; /* DKGRAY */ 313 + else if (yv < 192) idx = 7; /* LTGRAY */ 314 + else idx = 15; /* WHITE */ 315 + out[col] = (unsigned char)idx; 316 + } 317 + } 318 + return; 319 + } 320 + 321 + if (mode == MEDIA_DITHER_NEAREST) { 322 + /* Fused YUV→palette via yuv_to_pal_lut[]. The entire per-pixel 323 + * body below used to be ~11 arithmetic ops (chroma boost, 324 + * YUV→RGB with 4 mults, 3 clamps, LUT index); it's now one 325 + * shift-and-OR plus one indexed load. Same output because the 326 + * LUT was built by running exactly those ops at table-build 327 + * time. */ 328 + for (int row = 0; row < dh; row++) { 329 + int sr = row * sh / dh; 330 + const unsigned char *yrow = yp + sr * ys; 331 + const unsigned char *urow = up + (sr >> 1) * us; 332 + const unsigned char *vrow = vp + (sr >> 1) * vs; 333 + unsigned char *out = pal + row * dw; 334 + for (int col = 0; col < dw; col++) { 335 + int sc = sc_table[col]; 336 + int yv = yrow[sc]; 337 + int uv = urow[sc >> 1]; 338 + int vv = vrow[sc >> 1]; 339 + out[col] = yuv_to_pal_lut[((yv >> 3) << 10) 340 + | ((uv >> 3) << 5) 341 + | (vv >> 3)]; 342 + } 343 + } 344 + return; 345 + } 346 + 347 + /* Floyd-Steinberg error diffusion using the hoisted global buffers. */ 348 + int bufsz = (dw + 2) * 3; 349 + if (!ensure_err_buffers(bufsz)) { 350 + /* Allocation failed — degrade to nearest so we at least produce 351 + * a frame. Restore the mode after the recursive call so the next 352 + * frame can retry FLOYD. */ 353 + g_dither_mode = MEDIA_DITHER_NEAREST; 354 + dither_frame(yp, up, vp, ys, us, vs, sw, sh, pal, dw, dh); 355 + g_dither_mode = mode; 356 + return; 357 + } 358 + short *err_cur = g_err_cur; 359 + short *err_nxt = g_err_nxt; 360 + 361 + /* Clear err_cur once at the start of each frame — there's no 362 + * carry-over between frames. err_nxt gets memset per row inside 363 + * the loop, so it doesn't need a fresh init here. */ 364 + memset(err_cur, 0, bufsz * sizeof(short)); 365 + 366 + for (int row = 0; row < dh; row++) { 367 + int sr = row * sh / dh; 368 + const unsigned char *yrow = yp + sr * ys; 369 + const unsigned char *urow = up + (sr >> 1) * us; 370 + const unsigned char *vrow = vp + (sr >> 1) * vs; 371 + unsigned char *out = pal + row * dw; 372 + 373 + memset(err_nxt, 0, bufsz * sizeof(short)); 374 + 375 + for (int col = 0; col < dw; col++) { 376 + int sc = sc_table[col]; 377 + int yv = yrow[sc]; 378 + int uv = urow[sc >> 1] - 128; 379 + int vv = vrow[sc >> 1] - 128; 380 + 381 + /* Boost chroma — VGA palette is fully saturated */ 382 + uv = (uv * 3) >> 1; 383 + vv = vv * 3; 384 + 385 + /* YUV→RGB + accumulated error from neighbors */ 386 + int ei = (col + 1) * 3; /* +1 for left padding */ 387 + int r = clamp255(yv + ((359 * vv) >> 8) + err_cur[ei]); 388 + int g = clamp255(yv - ((88 * uv + 183 * vv) >> 8) + err_cur[ei + 1]); 389 + int b = clamp255(yv + ((454 * uv) >> 8) + err_cur[ei + 2]); 390 + 391 + /* Find nearest palette color via LUT */ 392 + int idx = rgb_to_pal_lut[(r >> 3) << 10 | (g >> 3) << 5 | (b >> 3)]; 393 + out[col] = (unsigned char)idx; 394 + 395 + /* Compute quantization error */ 396 + int er = r - tos_r[idx]; 397 + int eg = g - tos_g[idx]; 398 + int eb = b - tos_b[idx]; 399 + 400 + /* Distribute error (Floyd-Steinberg weights: 7/16, 3/16, 5/16, 1/16). 401 + * 402 + * Branch-free right-neighbor write: at col = dw-1, ri 403 + * indexes into the padding slot of err_cur (bufsz is 404 + * (dw+2)*3), which is harmless — the next row's memset 405 + * wipes it before any read. One fewer conditional per 406 + * pixel across the full frame. */ 407 + int ri = (col + 2) * 3; /* right neighbor (or padding) */ 408 + int li = col * 3; /* left-below neighbor */ 409 + int bi = (col + 1) * 3; /* below neighbor */ 410 + 411 + err_cur[ri] += (er * 7) >> 4; 412 + err_cur[ri + 1] += (eg * 7) >> 4; 413 + err_cur[ri + 2] += (eb * 7) >> 4; 414 + 415 + err_nxt[li] += (er * 3) >> 4; 416 + err_nxt[li + 1] += (eg * 3) >> 4; 417 + err_nxt[li + 2] += (eb * 3) >> 4; 418 + err_nxt[bi] += (er * 5) >> 4; 419 + err_nxt[bi + 1] += (eg * 5) >> 4; 420 + err_nxt[bi + 2] += (eb * 5) >> 4; 421 + err_nxt[ri] += er >> 4; 422 + err_nxt[ri + 1] += eg >> 4; 423 + err_nxt[ri + 2] += eb >> 4; 424 + } 425 + 426 + /* Swap rings: the row we just wrote into err_nxt becomes the 427 + * new err_cur for the next row. g_err_cur/g_err_nxt stay 428 + * consistent because we re-read them at the top of the next 429 + * frame — the in-loop swap is local-only. */ 430 + short *tmp = err_cur; err_cur = err_nxt; err_nxt = tmp; 431 + } 432 + 433 + /* No free — buffers stay allocated for the next frame. */ 434 + } 435 + 436 + /* Thin wrapper kept for any same-size callers. Both same-size and 437 + * scaled paths end up in dither_frame() above. */ 438 + static void yuv420_to_pal(const unsigned char *yp, const unsigned char *up, 439 + const unsigned char *vp, 440 + int ys, int us, int vs, 441 + int w, int h, unsigned char *pal) 442 + { 443 + dither_frame(yp, up, vp, ys, us, vs, w, h, pal, w, h); 444 + } 445 + 446 + /* ---- Decoder context ---- */ 447 + 448 + struct media_ctx { 449 + /* Source */ 450 + membuf_t mem; 451 + mov_reader_t *reader; 452 + unsigned char *readbuf; 453 + 454 + /* Video track ID — 0 means not yet discovered / absent. */ 455 + uint32_t video_track; 456 + 457 + /* Video track timescale from libmov (mdhd units per second). 458 + * Captured at open time; used to convert per-frame PTS from 459 + * track units to milliseconds. 0 means "unknown, don't pace". */ 460 + uint32_t video_timescale; 461 + 462 + /* Video */ 463 + int video_width, video_height; 464 + ISVCDecoder *h264_dec; 465 + int nalu_len_size; 466 + 467 + /* Current decoded output */ 468 + int last_type; /* MEDIA_VIDEO or 0 */ 469 + 470 + /* PTS of the most recent decoded frame in milliseconds. 471 + * Converted from libmov's track-unit PTS using video_timescale. 472 + * Readable via media_get_pts after media_next returns MEDIA_VIDEO. */ 473 + int64_t last_pts_ms; 474 + 475 + /* PTS of the frame immediately before the current one, in the 476 + * same ms units. Used to derive the dynamic skip threshold from 477 + * the observed source frame interval (last - prev). Rolls 478 + * forward on every on_frame call regardless of whether we 479 + * decoded or skipped, so the interval always reflects source 480 + * cadence, not the cadence of what we actually decoded. */ 481 + int64_t prev_pts_ms; 482 + 483 + /* Per-segment pacing clock used by the dynamic non-ref skip 484 + * logic. Both values anchor on the first frame of a segment: 485 + * segment_first_pts_ms grabs last_pts_ms, and 486 + * segment_start_wallclock_ms grabs sys_now()/ticks. Subsequent 487 + * frames compute lead = (pts - first_pts) - (now - start) and 488 + * use that to decide whether to skip this frame (if allowed). 489 + * first_pts_ms = -1 means "not yet anchored". */ 490 + int64_t segment_first_pts_ms; 491 + uint64_t segment_start_wallclock_ms; 492 + 493 + /* Count of non-reference frames the dynamic skip logic has 494 + * dropped in this context's lifetime. Exposed via 495 + * media_get_skip_count() so the playback summary can report 496 + * how many frames the dynamic pacing actually dropped. */ 497 + int64_t session_skip_count; 498 + 499 + /* Cached YUV plane pointers from the most recent successful 500 + * DecodeFrameNoDelay. These point into openh264's internal decoded- 501 + * picture buffer — the data is valid until the next decode call. 502 + * The fused path (media_next_pal) and the lazy paths (media_get_rgb, 503 + * media_get_pal) all read from here. */ 504 + unsigned char *yuv_y, *yuv_u, *yuv_v; 505 + int yuv_ys, yuv_us; 506 + int yuv_w, yuv_h; 507 + int yuv_valid; 508 + }; 509 + 510 + /* ---- AVCC SPS/PPS feeding ---- */ 511 + 512 + static int feed_sps_pps(media_ctx_t *ctx, const unsigned char *avcc, size_t avcc_len) { 513 + if (!avcc || avcc_len < 7) return -1; 514 + 515 + ctx->nalu_len_size = (avcc[4] & 0x03) + 1; 516 + 517 + unsigned char startcode[4] = {0, 0, 0, 1}; 518 + unsigned char *nal = NULL; 519 + size_t nal_cap = 0; 520 + 521 + /* SPS */ 522 + unsigned num_sps = avcc[5] & 0x1f; 523 + size_t off = 6; 524 + for (unsigned i = 0; i < num_sps && off + 2 <= avcc_len; i++) { 525 + unsigned len = ((unsigned)avcc[off] << 8) | avcc[off + 1]; 526 + off += 2; 527 + if (off + len > avcc_len) return -1; 528 + if (len + 4 > nal_cap) { nal_cap = len + 4; nal = realloc(nal, nal_cap); } 529 + memcpy(nal, startcode, 4); 530 + memcpy(nal + 4, avcc + off, len); 531 + unsigned char *pData[3] = {0}; 532 + SBufferInfo buf = {0}; 533 + (*ctx->h264_dec)->DecodeFrameNoDelay(ctx->h264_dec, nal, (int)(4 + len), pData, &buf); 534 + off += len; 535 + } 536 + 537 + /* PPS */ 538 + if (off >= avcc_len) { free(nal); return -1; } 539 + unsigned num_pps = avcc[off++]; 540 + for (unsigned i = 0; i < num_pps && off + 2 <= avcc_len; i++) { 541 + unsigned len = ((unsigned)avcc[off] << 8) | avcc[off + 1]; 542 + off += 2; 543 + if (off + len > avcc_len) { free(nal); return -1; } 544 + if (len + 4 > nal_cap) { nal_cap = len + 4; nal = realloc(nal, nal_cap); } 545 + memcpy(nal, startcode, 4); 546 + memcpy(nal + 4, avcc + off, len); 547 + unsigned char *pData[3] = {0}; 548 + SBufferInfo buf = {0}; 549 + (*ctx->h264_dec)->DecodeFrameNoDelay(ctx->h264_dec, nal, (int)(4 + len), pData, &buf); 550 + off += len; 551 + } 552 + free(nal); 553 + return 0; 554 + } 555 + 556 + /* ---- libmov track discovery callbacks ---- */ 557 + 558 + typedef struct { 559 + media_ctx_t *ctx; 560 + const void *avcc; 561 + size_t avcc_len; 562 + } track_info_t; 563 + 564 + static void on_video(void *param, uint32_t track, uint8_t object, 565 + int w, int h, const void *extra, size_t bytes) 566 + { 567 + track_info_t *ti = (track_info_t *)param; 568 + if (object == MOV_OBJECT_H264 && ti->ctx->video_track == 0) { 569 + ti->ctx->video_track = track; 570 + ti->ctx->video_width = w; 571 + ti->ctx->video_height = h; 572 + ti->avcc = extra; 573 + ti->avcc_len = bytes; 574 + } 575 + } 576 + 577 + /* ---- Frame decode callback for media_next() ---- */ 578 + 579 + static void on_frame(void *param, uint32_t track, const void *buffer, 580 + size_t bytes, int64_t pts, int64_t dts, int flags) 581 + { 582 + (void)dts; (void)flags; 583 + media_ctx_t *ctx = (media_ctx_t *)param; 584 + 585 + if (track == ctx->video_track && ctx->h264_dec) { 586 + /* Roll prev_pts_ms forward (becomes the previous frame's 587 + * PTS) so the dynamic skip threshold below sees the real 588 + * source frame interval, including across skipped frames. */ 589 + ctx->prev_pts_ms = ctx->last_pts_ms; 590 + 591 + /* Record the presentation time of this frame in milliseconds 592 + * so DecodeTask can pace playback against wallclock. Use PTS 593 + * (not DTS) because we want presentation order. If the track 594 + * timescale is unknown, write -1 as a sentinel so DecodeTask 595 + * can fall through to unpaced decode for this segment. */ 596 + if (ctx->video_timescale > 0) { 597 + ctx->last_pts_ms = (pts * 1000) / (int64_t)ctx->video_timescale; 598 + } else { 599 + ctx->last_pts_ms = -1; 600 + } 601 + 602 + const unsigned char *p = (const unsigned char *)buffer; 603 + int nls = ctx->nalu_len_size; 604 + unsigned char startcode[4] = {0, 0, 0, 1}; 605 + 606 + /* Dynamic non-reference frame skip. 607 + * 608 + * Master switch is g_skip_non_ref (pushed from the HolyC 609 + * pref). When OFF we decode every frame; when ON we drop 610 + * non-reference slices only if we're actually behind the 611 + * source timeline — the decision is per-frame and the 612 + * threshold adapts to the observed source frame interval. */ 613 + if (g_skip_non_ref && ctx->last_pts_ms >= 0) { 614 + /* Single wallclock read — reused for anchor and elapsed 615 + * so the first frame of a segment pays one call, not 616 + * two. */ 617 + uint64_t now_wallclock = media_ticks_ms(); 618 + 619 + /* Anchor the segment clock on the first valid frame. */ 620 + if (ctx->segment_first_pts_ms < 0) { 621 + ctx->segment_first_pts_ms = ctx->last_pts_ms; 622 + ctx->segment_start_wallclock_ms = now_wallclock; 623 + } 624 + 625 + int64_t target_ms = ctx->last_pts_ms - ctx->segment_first_pts_ms; 626 + int64_t elapsed_ms = (int64_t)(now_wallclock 627 + - ctx->segment_start_wallclock_ms); 628 + int64_t lead_ms = target_ms - elapsed_ms; 629 + 630 + /* Derive threshold from the observed source frame 631 + * interval. Covers 60fps (~16 ms -> -32 ms threshold), 632 + * 30fps (~33 ms -> -66 ms), 24fps (~41 ms -> -83 ms). 633 + * Falls back to a fixed value if we don't yet have 634 + * history or the interval is out of a sane range (VFR 635 + * jitter, segment boundary discontinuity, etc). */ 636 + int64_t threshold_ms = DEC_SKIP_THRESHOLD_FALLBACK_MS; 637 + if (ctx->prev_pts_ms >= 0) { 638 + int64_t interval = ctx->last_pts_ms - ctx->prev_pts_ms; 639 + if (interval >= DEC_SKIP_INTERVAL_MIN_MS && 640 + interval <= DEC_SKIP_INTERVAL_MAX_MS) { 641 + threshold_ms = DEC_SKIP_FRAME_MULT * interval; 642 + } 643 + } 644 + 645 + if (lead_ms < -threshold_ms) { 646 + /* Scan NALUs for the first VCL slice. If it's 647 + * nal_unit_type 1 (non-IDR) with nal_ref_idc == 0, 648 + * the frame is non-reference and safe to drop. We 649 + * return immediately on skip — media_next()'s outer 650 + * loop will re-enter us for the next frame since 651 + * last_type stays zero. */ 652 + size_t scan_pos = 0; 653 + while (scan_pos + (size_t)nls <= bytes) { 654 + unsigned nal_len = 0; 655 + for (int b = 0; b < nls; b++) 656 + nal_len = (nal_len << 8) | p[scan_pos + b]; 657 + scan_pos += nls; 658 + if (scan_pos + nal_len > bytes) break; 659 + if (nal_len > 0) { 660 + unsigned char hdr = p[scan_pos]; 661 + unsigned nal_unit_type = hdr & 0x1F; 662 + if (nal_unit_type == 1) { 663 + unsigned nal_ref_idc = (hdr >> 5) & 0x3; 664 + if (nal_ref_idc == 0) { 665 + ctx->session_skip_count++; 666 + return; 667 + } 668 + /* First VCL was reference — stop scanning 669 + * and fall through to the decode loop. */ 670 + break; 671 + } 672 + } 673 + scan_pos += nal_len; 674 + } 675 + } 676 + } 677 + 678 + /* Decode H.264 AVCC NALUs */ 679 + size_t pos = 0; 680 + 681 + while (pos + (size_t)nls <= bytes) { 682 + unsigned nal_len = 0; 683 + for (int b = 0; b < nls; b++) 684 + nal_len = (nal_len << 8) | p[pos + b]; 685 + pos += nls; 686 + if (pos + nal_len > bytes) break; 687 + 688 + /* Build Annex B NAL in readbuf (reuse) */ 689 + if (nal_len + 4 <= 2 * 1024 * 1024) { 690 + memcpy(ctx->readbuf, startcode, 4); 691 + memcpy(ctx->readbuf + 4, p + pos, nal_len); 692 + 693 + unsigned char *pData[3] = {0}; 694 + SBufferInfo bufInfo = {0}; 695 + (*ctx->h264_dec)->DecodeFrameNoDelay(ctx->h264_dec, 696 + ctx->readbuf, (int)(4 + nal_len), pData, &bufInfo); 697 + 698 + if (bufInfo.iBufferStatus == 1 && pData[0] && pData[1] && pData[2]) { 699 + int w = bufInfo.UsrData.sSystemBuffer.iWidth; 700 + int h = bufInfo.UsrData.sSystemBuffer.iHeight; 701 + int ys = bufInfo.UsrData.sSystemBuffer.iStride[0]; 702 + int us = bufInfo.UsrData.sSystemBuffer.iStride[1]; 703 + 704 + /* Cache YUV pointers only. We used to do a full 705 + * per-frame yuv420_to_rgb pass here for get_rgb 706 + * compatibility, but nothing in the HolyC layer 707 + * ever reads the RGB cache — media_next_pal is 708 + * the sole hot path and it reads YUV directly. 709 + * Killing the wasted conversion removes 710 + * w*h pixels of arithmetic plus 3*w*h bytes of 711 + * memory write per frame (at 1080p: ~25 MB/sec 712 + * of wasted memory bandwidth). get_rgb and 713 + * get_pal are now lazy — they convert from the 714 + * cached YUV planes on demand. */ 715 + ctx->yuv_y = pData[0]; 716 + ctx->yuv_u = pData[1]; 717 + ctx->yuv_v = pData[2]; 718 + ctx->yuv_ys = ys; 719 + ctx->yuv_us = us; 720 + ctx->yuv_w = w; 721 + ctx->yuv_h = h; 722 + ctx->yuv_valid = 1; 723 + 724 + ctx->last_type = MEDIA_VIDEO; 725 + } 726 + } 727 + pos += nal_len; 728 + } 729 + } 730 + } 731 + 732 + /* ---- Public API ---- */ 733 + 734 + media_ctx_t *media_open(const unsigned char *data, size_t len) { 735 + media_ctx_t *ctx = calloc(1, sizeof(*ctx)); 736 + if (!ctx) return NULL; 737 + 738 + /* Sentinel: no frame decoded yet. Flips to either a real ms 739 + * value on first on_frame, or stays -1 if timescale was never 740 + * discovered (so DecodeTask falls through to unpaced decode). */ 741 + ctx->last_pts_ms = -1; 742 + ctx->prev_pts_ms = -1; 743 + 744 + /* Pacing clock starts unanchored — on_frame will stamp it on 745 + * the first frame whose timescale-scaled PTS is valid. */ 746 + ctx->segment_first_pts_ms = -1; 747 + 748 + ctx->mem.data = data; 749 + ctx->mem.len = len; 750 + ctx->mem.pos = 0; 751 + 752 + ctx->reader = mov_reader_create(&membuf_vtable, &ctx->mem); 753 + if (!ctx->reader) { free(ctx); return NULL; } 754 + 755 + ctx->readbuf = malloc(2 * 1024 * 1024); 756 + if (!ctx->readbuf) { mov_reader_destroy(ctx->reader); free(ctx); return NULL; } 757 + 758 + /* Discover tracks. on_audio is NULL so libmov will silently skip 759 + * any audio tracks in the file — we only care about video. */ 760 + track_info_t ti = { .ctx = ctx }; 761 + struct mov_reader_trackinfo_t trackinfo = { on_video, NULL, NULL }; 762 + mov_reader_getinfo(ctx->reader, &trackinfo, &ti); 763 + 764 + /* Grab the video track's mdhd timescale so on_frame can convert 765 + * per-frame PTS into milliseconds for DecodeTask's pacing logic. 766 + * Zero means the track wasn't found or the MP4 didn't declare 767 + * a timescale — in that case last_pts_ms stays 0 and the caller 768 + * falls back to unpaced decode. */ 769 + if (ctx->video_track != 0) { 770 + ctx->video_timescale = mov_reader_get_track_timescale(ctx->reader, 771 + ctx->video_track); 772 + } 773 + 774 + /* Init H.264 decoder if video track found */ 775 + if (ctx->video_track != 0) { 776 + if (WelsCreateDecoder(&ctx->h264_dec) == 0 && ctx->h264_dec) { 777 + SDecodingParam dp = {0}; 778 + dp.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC; 779 + if ((*ctx->h264_dec)->Initialize(ctx->h264_dec, &dp) != 0) { 780 + WelsDestroyDecoder(ctx->h264_dec); 781 + ctx->h264_dec = NULL; 782 + } else { 783 + /* Error concealment costs a few percent on clean streams 784 + * because it sets up MV-copy fallback state on every 785 + * frame even when nothing is ever missing. Turn it off. */ 786 + int ec = (int)ERROR_CON_DISABLE; 787 + (*ctx->h264_dec)->SetOption(ctx->h264_dec, 788 + DECODER_OPTION_ERROR_CON_IDC, &ec); 789 + if (ti.avcc) { 790 + feed_sps_pps(ctx, ti.avcc, ti.avcc_len); 791 + } 792 + } 793 + } 794 + } 795 + 796 + if (ctx->video_track == 0) { 797 + media_close(ctx); 798 + return NULL; 799 + } 800 + 801 + return ctx; 802 + } 803 + 804 + void media_close(media_ctx_t *ctx) { 805 + if (!ctx) return; 806 + if (ctx->h264_dec) { 807 + (*ctx->h264_dec)->Uninitialize(ctx->h264_dec); 808 + WelsDestroyDecoder(ctx->h264_dec); 809 + } 810 + if (ctx->reader) mov_reader_destroy(ctx->reader); 811 + free(ctx->readbuf); 812 + free(ctx); 813 + } 814 + 815 + int media_video_info(media_ctx_t *ctx, int *w, int *h) { 816 + if (!ctx || ctx->video_track == 0) return -1; 817 + *w = ctx->video_width; 818 + *h = ctx->video_height; 819 + return 0; 820 + } 821 + 822 + /* PTS of the last decoded frame in milliseconds. Only meaningful 823 + * after media_next / media_next_pal returns MEDIA_VIDEO. Returns 0 824 + * if the video_timescale wasn't discoverable, in which case the 825 + * caller should skip PTS-based pacing. */ 826 + int64_t media_get_pts(media_ctx_t *ctx) { 827 + if (!ctx) return 0; 828 + return ctx->last_pts_ms; 829 + } 830 + 831 + /* Cumulative count of non-reference frames the dynamic pacing 832 + * logic has skipped since media_open. Useful for the post- 833 + * playback summary so users can see how many frames were 834 + * actually dropped. */ 835 + int64_t media_get_skip_count(media_ctx_t *ctx) { 836 + if (!ctx) return 0; 837 + return ctx->session_skip_count; 838 + } 839 + 840 + int media_next(media_ctx_t *ctx) { 841 + if (!ctx) return MEDIA_ERROR; 842 + 843 + /* Read frames until we produce decoded output */ 844 + for (;;) { 845 + ctx->last_type = 0; 846 + int r = mov_reader_read(ctx->reader, ctx->readbuf, 2 * 1024 * 1024, 847 + on_frame, ctx); 848 + if (r == 0) return MEDIA_EOF; 849 + if (r < 0) return MEDIA_ERROR; 850 + if (ctx->last_type != 0) return ctx->last_type; 851 + /* Frame was read but no output produced (e.g. H.264 buffering) — keep reading */ 852 + } 853 + } 854 + 855 + /* Lazy RGB conversion. Was previously eager (every decoded frame was 856 + * converted into a cached rgb_buf) but nothing in the VodBrowser HolyC 857 + * layer ever reads the RGB cache — media_next_pal is the sole hot 858 + * path. Now we convert from the cached YUV planes only on demand. */ 859 + int media_get_rgb(media_ctx_t *ctx, unsigned char *rgb, int *w, int *h) { 860 + if (!ctx || !ctx->yuv_valid) return -1; 861 + *w = ctx->yuv_w; 862 + *h = ctx->yuv_h; 863 + yuv420_to_rgb(ctx->yuv_y, ctx->yuv_u, ctx->yuv_v, 864 + ctx->yuv_ys, ctx->yuv_us, ctx->yuv_us, 865 + ctx->yuv_w, ctx->yuv_h, rgb); 866 + return 0; 867 + } 868 + 869 + /* media_get_pal: dither the currently-cached YUV frame into the 870 + * caller's pal buffer. Mirrors media_next_pal's target-size handling: 871 + * if the caller's *w and *h are non-zero and smaller than the source, 872 + * downscale into them; otherwise emit at source size. Used by the 873 + * paced DecodeTask loop after a standalone media_next() call, so 874 + * skipping a frame's dither+blit step is cheap and doesn't also 875 + * skip the decode. */ 876 + int media_get_pal(media_ctx_t *ctx, unsigned char *pal, int *w, int *h) { 877 + if (!ctx || !ctx->yuv_valid) return -1; 878 + int sw = ctx->yuv_w, sh = ctx->yuv_h; 879 + int dw = *w, dh = *h; 880 + if (dw <= 0 || dh <= 0 || (dw >= sw && dh >= sh)) { 881 + dw = sw; dh = sh; 882 + } 883 + 884 + build_pal_lut(); 885 + dither_frame(ctx->yuv_y, ctx->yuv_u, ctx->yuv_v, 886 + ctx->yuv_ys, ctx->yuv_us, ctx->yuv_us, 887 + sw, sh, pal, dw, dh); 888 + *w = dw; *h = dh; 889 + return 0; 890 + } 891 + 892 + /* Fused demux→decode→dither in one call. Returns MEDIA_VIDEO/EOF/ERROR. 893 + * On MEDIA_VIDEO, pal is filled with 8bpp palette indices. */ 894 + /* Fused demux→decode→scale→dither. 895 + * If *w and *h are non-zero on input, scales to that target size. 896 + * If zero, uses full video resolution. 897 + * On output, *w and *h are set to the actual output dimensions. */ 898 + int media_next_pal(media_ctx_t *ctx, unsigned char *pal, int *w, int *h) { 899 + if (!ctx) return MEDIA_ERROR; 900 + 901 + build_pal_lut(); 902 + int target_w = *w, target_h = *h; 903 + 904 + for (;;) { 905 + ctx->last_type = 0; 906 + ctx->yuv_valid = 0; 907 + int r = mov_reader_read(ctx->reader, ctx->readbuf, 2 * 1024 * 1024, 908 + on_frame, ctx); 909 + if (r == 0) return MEDIA_EOF; 910 + if (r < 0) return MEDIA_ERROR; 911 + 912 + if (ctx->last_type == MEDIA_VIDEO && ctx->yuv_valid) { 913 + int sw = ctx->yuv_w, sh = ctx->yuv_h; 914 + int dw = target_w, dh = target_h; 915 + 916 + /* Choose output dimensions: if the caller's target is zero 917 + * or larger than the source, emit at source size. Otherwise 918 + * dither straight to the target (downsample happens inside 919 + * dither_frame's inner loop via the sr/sc arithmetic). */ 920 + if (dw <= 0 || dh <= 0 || (dw >= sw && dh >= sh)) { 921 + dw = sw; dh = sh; 922 + } 923 + 924 + dither_frame(ctx->yuv_y, ctx->yuv_u, ctx->yuv_v, 925 + ctx->yuv_ys, ctx->yuv_us, ctx->yuv_us, 926 + sw, sh, pal, dw, dh); 927 + 928 + *w = dw; *h = dh; 929 + return MEDIA_VIDEO; 930 + } 931 + if (ctx->last_type != 0) return ctx->last_type; 932 + } 933 + } 934 + 935 + /* The canonical vtable handed to HolyC lives in media_entry.c's 936 + * static `api` — this module just exports the individual function 937 + * pointers for that file to reference. */
+115
media/media_api.h
··· 1 + /* 2 + * media_api.h — VodBrowser media library API 3 + * 4 + * Bundles libmov + openh264 behind a streaming decode interface. 5 + * This header defines the thunk boundary: HolyC calls these functions 6 + * via the function table returned by media_api_init(). 7 + * 8 + * Video-only. Real PCM audio output isn't feasible on TempleOS V5.03 9 + * — the PC speaker doesn't support PWM in QEMU, the SB16 DAC path is 10 + * stubbed, and there's no sub-16MB allocator for ISA DMA. The vtable 11 + * is purely video + palette dithering. 12 + */ 13 + #ifndef VODBROWSER_MEDIA_API_H 14 + #define VODBROWSER_MEDIA_API_H 15 + 16 + #include <stddef.h> 17 + #include <stdint.h> 18 + 19 + /* Frame types returned by media_next() */ 20 + #define MEDIA_VIDEO 1 21 + #define MEDIA_EOF 0 22 + #define MEDIA_ERROR -1 23 + 24 + /* Opaque decoder context */ 25 + typedef struct media_ctx media_ctx_t; 26 + 27 + /* Open MP4 data for decoding. Handles both regular and fragmented MP4. 28 + * The data buffer must remain valid for the lifetime of the context. 29 + * Returns NULL on failure. */ 30 + media_ctx_t *media_open(const unsigned char *data, size_t len); 31 + 32 + /* Close decoder and free all resources. */ 33 + void media_close(media_ctx_t *ctx); 34 + 35 + /* Get video track dimensions. Returns 0 on success, -1 if no video track. */ 36 + int media_video_info(media_ctx_t *ctx, int *width, int *height); 37 + 38 + /* Presentation timestamp of the most recently decoded frame, in 39 + * milliseconds from the track timeline origin. Only meaningful after 40 + * media_next / media_next_pal returns MEDIA_VIDEO. Returns 0 if the 41 + * track timescale was unknown at open time (caller should fall back 42 + * to unpaced decode in that case). */ 43 + int64_t media_get_pts(media_ctx_t *ctx); 44 + 45 + /* Advance to next decoded frame. Returns MEDIA_VIDEO, MEDIA_EOF, or 46 + * MEDIA_ERROR. Call media_get_rgb / media_get_pal after a MEDIA_VIDEO 47 + * return to read out the decoded frame. */ 48 + int media_next(media_ctx_t *ctx); 49 + 50 + /* Copy current video frame as RGB24 into caller's buffer. 51 + * Buffer must be at least width * height * 3 bytes. 52 + * Only valid after media_next() returned MEDIA_VIDEO. */ 53 + int media_get_rgb(media_ctx_t *ctx, unsigned char *rgb, int *width, int *height); 54 + 55 + /* Copy current video frame as 8bpp palette indices (TempleOS 16-color). 56 + * Dithers the cached YUV planes to the 16-color VGA palette. Buffer 57 + * must be at least width * height bytes. Only valid after media_next() 58 + * returned MEDIA_VIDEO. */ 59 + int media_get_pal(media_ctx_t *ctx, unsigned char *pal, int *width, int *height); 60 + 61 + /* Fused demux -> decode -> scale -> dither. Returns MEDIA_VIDEO, MEDIA_EOF, 62 + * or MEDIA_ERROR. If *w and *h are non-zero on input, scales to that 63 + * target size; if zero, uses full video resolution. On output, *w and 64 + * *h are the actual emitted dimensions. This is the hot path the 65 + * playback loop uses — skips the wasted YUV->RGB conversion that 66 + * plain media_next() does. */ 67 + int media_next_pal(media_ctx_t *ctx, unsigned char *pal, int *width, int *height); 68 + 69 + /* Dither mode passed to media_set_dither_mode(). Selects which inner 70 + * loop media_next_pal runs when converting YUV -> 8bpp palette. */ 71 + #define MEDIA_DITHER_FLOYD 0 /* Floyd-Steinberg + saturation LUT (default) */ 72 + #define MEDIA_DITHER_NEAREST 1 /* nearest-color LUT lookup, no dither */ 73 + #define MEDIA_DITHER_GRAY 2 /* luma quantised to 4 gray levels */ 74 + 75 + /* Select the dither mode used by media_next_pal. Safe to call at any 76 + * time — the setting is a plain int and takes effect on the next 77 + * frame. Default is MEDIA_DITHER_FLOYD (backward-compatible). */ 78 + void media_set_dither_mode(int mode); 79 + 80 + /* Master switch for dynamic non-reference frame skipping. When 81 + * enabled, on_frame compares each frame's PTS against wallclock 82 + * (via sys_now / media_ticks_ms) and drops non-reference frames 83 + * ONLY when we're falling behind the source timeline. When 84 + * disabled, every frame is decoded. This is a pref the caller 85 + * flips; the actual per-frame decision is made inside on_frame. */ 86 + void media_set_skip_non_ref(int enabled); 87 + 88 + /* Cumulative count of non-reference frames the dynamic skip logic 89 + * has dropped since media_open was called. Readable at any time; 90 + * used by the playback summary to report drop ratios. */ 91 + int64_t media_get_skip_count(media_ctx_t *ctx); 92 + 93 + /* ---- Function table for HolyC thunking ---- 94 + * 95 + * Layout is positional — HolyC reads by index, so the order here has 96 + * to match the Media.HC MEDIA_FN_* defines in lockstep. When you add 97 + * or remove a slot, update both files. */ 98 + struct media_api { 99 + media_ctx_t *(*open)(const unsigned char *data, size_t len); /* 0 */ 100 + void (*close)(media_ctx_t *ctx); /* 1 */ 101 + int (*video_info)(media_ctx_t *ctx, int *w, int *h); /* 2 */ 102 + int (*next)(media_ctx_t *ctx); /* 3 */ 103 + int (*get_rgb)(media_ctx_t *ctx, unsigned char *rgb, 104 + int *w, int *h); /* 4 */ 105 + int (*get_pal)(media_ctx_t *ctx, unsigned char *pal, 106 + int *w, int *h); /* 5 */ 107 + int (*next_pal)(media_ctx_t *ctx, unsigned char *pal, 108 + int *w, int *h); /* 6 */ 109 + void (*set_dither_mode)(int mode); /* 7 */ 110 + int64_t (*get_pts)(media_ctx_t *ctx); /* 8 */ 111 + void (*set_skip_non_ref)(int enabled); /* 9 */ 112 + int64_t (*get_skip_count)(media_ctx_t *ctx); /* 10 */ 113 + }; 114 + 115 + #endif
+55
media/media_entry.c
··· 1 + /* 2 + * media_entry.c — ELF entry point for Media.prg 3 + * 4 + * Historical note: this file used to switch %rsp to an 8MB heap buffer 5 + * via call_on_big_stack before every C call because openh264 is 6 + * stack-hungry. That caused TempleOS WallPaper to panic "Stk Overflow" 7 + * when the timer IRQ preempted us with rsp pointing into the heap — 8 + * TempleOS's UnusedStk(task) check requires rsp to be inside the 9 + * task's official stack range. Fix: run openh264 directly on the 10 + * caller's task stack. Callers MUST Spawn DecodeTask with a large 11 + * stk_size (8MB used by VodBrowser.HC). 12 + */ 13 + #include "media_api.h" 14 + #include <stddef.h> 15 + #include <stdint.h> 16 + 17 + /* API vtable handed back to HolyC. All slots point directly at the 18 + * corresponding C function — no wrapping, no stack switching. */ 19 + static const struct media_api api = { 20 + .open = media_open, 21 + .close = media_close, 22 + .video_info = media_video_info, 23 + .next = media_next, 24 + .get_rgb = media_get_rgb, 25 + .get_pal = media_get_pal, 26 + .next_pal = media_next_pal, 27 + .set_dither_mode = media_set_dither_mode, 28 + .get_pts = media_get_pts, 29 + .set_skip_non_ref = media_set_skip_non_ref, 30 + .get_skip_count = media_get_skip_count, 31 + }; 32 + 33 + /* ELF entry point — called by TempleOS loader. arg1 is an array of 34 + * pointers: [0]=tos_malloc, [1]=tos_free, [2]=tos_ticks_ms. 35 + * The ticks callback (slot 2) feeds sys_now() inside media_minilib 36 + * so DecodeTask can pace decode against wallclock. */ 37 + void *elf_main(void *arg1, void *arg2) 38 + { 39 + (void)arg2; 40 + 41 + extern void media_minilib_set_callbacks( 42 + void *(*malloc_fn)(uint64_t), 43 + void (*free_fn)(void *), 44 + uint64_t (*ticks_fn)(void)); 45 + 46 + if (arg1) { 47 + uint64_t *args = arg1; 48 + void *(*tos_malloc)(uint64_t) = (void *(*)(uint64_t))args[0]; 49 + void (*tos_free)(void *) = (void (*)(void *))args[1]; 50 + uint64_t (*tos_ticks)(void) = (uint64_t (*)(void))args[2]; 51 + media_minilib_set_callbacks(tos_malloc, tos_free, tos_ticks); 52 + } 53 + 54 + return (void *)&api; 55 + }
+510
media/media_minilib.c
··· 1 + /* 2 + * media_minilib.c — Minimal libc + C++ runtime stubs for Media.prg (freestanding) 3 + * 4 + * Based on net/minilib.c. Adds C++ operator new/delete, __cxa_* stubs, 5 + * and pthread stubs (so openh264 falls back to single-threaded decoding). 6 + */ 7 + #include <stddef.h> 8 + #include <stdint.h> 9 + 10 + /* Forward declarations */ 11 + void *memset(void *s, int c, size_t n); 12 + void *memcpy(void *dest, const void *src, size_t n); 13 + void free(void *ptr); 14 + 15 + /* ---- TempleOS callbacks (set during init) ---- */ 16 + 17 + static uint64_t (*cb_rand)(void); 18 + static uint64_t (*cb_ticks)(void); 19 + static void *(*cb_malloc)(uint64_t); 20 + static void (*cb_free)(void *); 21 + 22 + /* Full callback init (used by unified All.prg) */ 23 + void minilib_set_callbacks( 24 + uint64_t (*rand_fn)(void), 25 + uint64_t (*ticks_fn)(void), 26 + void *(*malloc_fn)(uint64_t), 27 + void (*free_fn)(void *) 28 + ) { 29 + cb_rand = rand_fn; 30 + cb_ticks = ticks_fn; 31 + cb_malloc = malloc_fn; 32 + cb_free = free_fn; 33 + } 34 + 35 + /* Compat wrapper for standalone Media.prg */ 36 + void media_minilib_set_callbacks( 37 + void *(*malloc_fn)(uint64_t), 38 + void (*free_fn)(void *), 39 + uint64_t (*ticks_fn)(void) 40 + ) { 41 + cb_malloc = malloc_fn; 42 + cb_free = free_fn; 43 + cb_ticks = ticks_fn; 44 + } 45 + 46 + /* Full-width wallclock accessor for media_api.c's dynamic pacing. 47 + * sys_now() returns unsigned int which truncates to 32 bits and 48 + * would wrap every ~49 days; this returns the full uint64_t so 49 + * long sessions don't do weird modular arithmetic. */ 50 + uint64_t media_ticks_ms(void) { 51 + if (cb_ticks) return cb_ticks(); 52 + return 0; 53 + } 54 + 55 + /* ---- Memory ---- */ 56 + 57 + /* 16-byte header stored immediately before every malloc'd pointer. 58 + * Records the user-visible allocation size so realloc() can copy 59 + * min(old, new) bytes instead of reading past the old buffer. The 60 + * 16-byte size also keeps returned pointers 16-byte aligned, which 61 + * is the alignment C code generally expects for large allocations. */ 62 + typedef struct { size_t user_size; size_t _pad; } mm_header; 63 + 64 + void *malloc(size_t size) { 65 + if (!cb_malloc) return NULL; 66 + mm_header *h = cb_malloc((uint64_t)(sizeof(mm_header) + size)); 67 + if (!h) return NULL; 68 + h->user_size = size; 69 + return (void *)(h + 1); 70 + } 71 + 72 + void *calloc(size_t n, size_t size) { 73 + size_t total = n * size; 74 + void *p = malloc(total); 75 + if (p) memset(p, 0, total); 76 + return p; 77 + } 78 + 79 + void *realloc(void *ptr, size_t size) { 80 + if (!ptr) return malloc(size); 81 + mm_header *h = (mm_header *)ptr - 1; 82 + size_t old_size = h->user_size; 83 + void *newp = malloc(size); 84 + if (newp) { 85 + size_t n = old_size < size ? old_size : size; 86 + memcpy(newp, ptr, n); 87 + free(ptr); 88 + } 89 + return newp; 90 + } 91 + 92 + void free(void *ptr) { 93 + if (!cb_free || !ptr) return; 94 + cb_free((mm_header *)ptr - 1); 95 + } 96 + 97 + /* ---- String/memory functions ---- */ 98 + 99 + void *memcpy(void *dest, const void *src, size_t n) { 100 + unsigned char *d = dest; 101 + const unsigned char *s = src; 102 + while (n--) *d++ = *s++; 103 + return dest; 104 + } 105 + 106 + void *memmove(void *dest, const void *src, size_t n) { 107 + unsigned char *d = dest; 108 + const unsigned char *s = src; 109 + if (d < s) { 110 + while (n--) *d++ = *s++; 111 + } else { 112 + d += n; s += n; 113 + while (n--) *--d = *--s; 114 + } 115 + return dest; 116 + } 117 + 118 + void *memset(void *s, int c, size_t n) { 119 + unsigned char *p = s; 120 + while (n--) *p++ = (unsigned char)c; 121 + return s; 122 + } 123 + 124 + int memcmp(const void *s1, const void *s2, size_t n) { 125 + const unsigned char *a = s1, *b = s2; 126 + while (n--) { 127 + if (*a != *b) return *a - *b; 128 + a++; b++; 129 + } 130 + return 0; 131 + } 132 + 133 + size_t strlen(const char *s) { 134 + const char *p = s; 135 + while (*p) p++; 136 + return (size_t)(p - s); 137 + } 138 + 139 + char *strcpy(char *dest, const char *src) { 140 + char *d = dest; 141 + while ((*d++ = *src++)); 142 + return dest; 143 + } 144 + 145 + char *strncpy(char *dest, const char *src, size_t n) { 146 + char *d = dest; 147 + while (n && (*d++ = *src++)) n--; 148 + while (n--) *d++ = 0; 149 + return dest; 150 + } 151 + 152 + int strcmp(const char *s1, const char *s2) { 153 + while (*s1 && *s1 == *s2) { s1++; s2++; } 154 + return *(unsigned char *)s1 - *(unsigned char *)s2; 155 + } 156 + 157 + int strncmp(const char *s1, const char *s2, size_t n) { 158 + while (n && *s1 && *s1 == *s2) { s1++; s2++; n--; } 159 + return n ? *(unsigned char *)s1 - *(unsigned char *)s2 : 0; 160 + } 161 + 162 + char *strchr(const char *s, int c) { 163 + while (*s) { 164 + if (*s == (char)c) return (char *)s; 165 + s++; 166 + } 167 + return (c == 0) ? (char *)s : NULL; 168 + } 169 + 170 + char *strrchr(const char *s, int c) { 171 + const char *last = NULL; 172 + while (*s) { 173 + if (*s == (char)c) last = s; 174 + s++; 175 + } 176 + return (c == 0) ? (char *)s : (char *)last; 177 + } 178 + 179 + char *strstr(const char *haystack, const char *needle) { 180 + if (!*needle) return (char *)haystack; 181 + size_t nlen = strlen(needle); 182 + while (*haystack) { 183 + if (strncmp(haystack, needle, nlen) == 0) return (char *)haystack; 184 + haystack++; 185 + } 186 + return NULL; 187 + } 188 + 189 + int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } 190 + int isxdigit(int c) { 191 + return (c >= '0' && c <= '9') || 192 + (c >= 'a' && c <= 'f') || 193 + (c >= 'A' && c <= 'F'); 194 + } 195 + int toupper(int c) { return (c >= 'a' && c <= 'z') ? c - 32 : c; } 196 + int isdigit(int c) { return c >= '0' && c <= '9'; } 197 + int isalpha(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } 198 + int isspace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } 199 + 200 + long strtol(const char *s, char **endp, int base) { 201 + long result = 0; 202 + int neg = 0; 203 + while (isspace(*s)) s++; 204 + if (*s == '-') { neg = 1; s++; } 205 + else if (*s == '+') s++; 206 + if (base == 0) { 207 + if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } 208 + else if (*s == '0') { base = 8; } 209 + else base = 10; 210 + } 211 + while (*s) { 212 + int d; 213 + if (*s >= '0' && *s <= '9') d = *s - '0'; 214 + else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; 215 + else if (*s >= 'A' && *s <= 'F') d = *s - 'A' + 10; 216 + else break; 217 + if (d >= base) break; 218 + result = result * base + d; 219 + s++; 220 + } 221 + if (endp) *endp = (char *)s; 222 + return neg ? -result : result; 223 + } 224 + 225 + int atoi(const char *s) { return (int)strtol(s, NULL, 10); } 226 + 227 + /* ---- Formatted output (minimal) ---- */ 228 + 229 + static int fmt_int(char *buf, size_t sz, size_t *pos, long long val, int base, int upper) { 230 + char tmp[24]; 231 + int neg = val < 0 && base == 10; 232 + unsigned long long uval = neg ? -(unsigned long long)val : (unsigned long long)val; 233 + int i = 0; 234 + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; 235 + if (uval == 0) tmp[i++] = '0'; 236 + while (uval) { tmp[i++] = digits[uval % base]; uval /= base; } 237 + if (neg) tmp[i++] = '-'; 238 + int written = 0; 239 + while (i--) { 240 + if (*pos < sz - 1) buf[(*pos)++] = tmp[i]; 241 + written++; 242 + } 243 + return written; 244 + } 245 + 246 + int vsnprintf(char *buf, size_t sz, const char *fmt, __builtin_va_list ap) { 247 + size_t pos = 0; 248 + int total = 0; 249 + while (*fmt) { 250 + if (*fmt != '%') { 251 + if (pos < sz - 1) buf[pos++] = *fmt; 252 + fmt++; total++; continue; 253 + } 254 + fmt++; 255 + int is_long = 0, is_size = 0; 256 + if (*fmt == 'l') { is_long = 1; fmt++; if (*fmt == 'l') fmt++; } 257 + if (*fmt == 'z') { is_size = 1; fmt++; } 258 + switch (*fmt) { 259 + case 's': { 260 + const char *s = __builtin_va_arg(ap, const char *); 261 + if (!s) s = "(null)"; 262 + while (*s) { if (pos < sz - 1) buf[pos++] = *s; s++; total++; } 263 + break; 264 + } 265 + case 'd': case 'i': { 266 + long long v = is_size ? (long long)__builtin_va_arg(ap, size_t) 267 + : is_long ? __builtin_va_arg(ap, long long) 268 + : (long long)__builtin_va_arg(ap, int); 269 + total += fmt_int(buf, sz, &pos, v, 10, 0); 270 + break; 271 + } 272 + case 'u': { 273 + unsigned long long v = is_size ? (unsigned long long)__builtin_va_arg(ap, size_t) 274 + : is_long ? __builtin_va_arg(ap, unsigned long long) 275 + : (unsigned long long)__builtin_va_arg(ap, unsigned int); 276 + total += fmt_int(buf, sz, &pos, (long long)v, 10, 0); 277 + break; 278 + } 279 + case 'x': case 'X': { 280 + unsigned long long v = is_size ? (unsigned long long)__builtin_va_arg(ap, size_t) 281 + : is_long ? __builtin_va_arg(ap, unsigned long long) 282 + : (unsigned long long)__builtin_va_arg(ap, unsigned int); 283 + total += fmt_int(buf, sz, &pos, (long long)v, 16, *fmt == 'X'); 284 + break; 285 + } 286 + case 'p': { 287 + void *p = __builtin_va_arg(ap, void *); 288 + total += fmt_int(buf, sz, &pos, (long long)(uintptr_t)p, 16, 0); 289 + break; 290 + } 291 + case 'c': { 292 + char c = (char)__builtin_va_arg(ap, int); 293 + if (pos < sz - 1) buf[pos++] = c; 294 + total++; 295 + break; 296 + } 297 + case '%': 298 + if (pos < sz - 1) buf[pos++] = '%'; 299 + total++; 300 + break; 301 + default: 302 + if (pos < sz - 1) buf[pos++] = *fmt; 303 + total++; 304 + break; 305 + } 306 + fmt++; 307 + } 308 + if (sz > 0) buf[pos < sz ? pos : sz - 1] = '\0'; 309 + return total; 310 + } 311 + 312 + int snprintf(char *buf, size_t sz, const char *fmt, ...) { 313 + __builtin_va_list ap; 314 + __builtin_va_start(ap, fmt); 315 + int r = vsnprintf(buf, sz, fmt, ap); 316 + __builtin_va_end(ap); 317 + return r; 318 + } 319 + 320 + int sprintf(char *buf, const char *fmt, ...) { 321 + __builtin_va_list ap; 322 + __builtin_va_start(ap, fmt); 323 + int r = vsnprintf(buf, 0x7FFFFFFF, fmt, ap); 324 + __builtin_va_end(ap); 325 + return r; 326 + } 327 + 328 + /* ---- File I/O stubs (openh264 logging tries these) ---- */ 329 + 330 + typedef struct { int dummy; } FILE; 331 + static FILE _stdout_dummy; 332 + static FILE _stderr_dummy; 333 + FILE *stdout = &_stdout_dummy; 334 + FILE *stderr = &_stderr_dummy; 335 + 336 + int fprintf(FILE *f, const char *fmt, ...) { (void)f; (void)fmt; return 0; } 337 + int printf(const char *fmt, ...) { (void)fmt; return 0; } 338 + int fflush(FILE *f) { (void)f; return 0; } 339 + int fputs(const char *s, FILE *f) { (void)s; (void)f; return 0; } 340 + int fputc(int c, FILE *f) { (void)c; (void)f; return 0; } 341 + FILE *fopen(const char *path, const char *mode) { (void)path; (void)mode; return NULL; } 342 + int fclose(FILE *f) { (void)f; return 0; } 343 + int fseek(FILE *f, long off, int whence) { (void)f; (void)off; (void)whence; return -1; } 344 + size_t fread(void *buf, size_t s, size_t n, FILE *f) { (void)buf; (void)s; (void)n; (void)f; return 0; } 345 + size_t fwrite(const void *buf, size_t s, size_t n, FILE *f) { (void)buf; (void)s; (void)n; (void)f; return n; } 346 + 347 + /* ---- errno ---- */ 348 + 349 + static int errno_val; 350 + int *__errno_location(void) { return &errno_val; } 351 + 352 + /* ---- bcmp ---- */ 353 + 354 + int bcmp(const void *s1, const void *s2, size_t n) { return memcmp(s1, s2, n); } 355 + 356 + /* ---- time stubs ---- */ 357 + 358 + typedef long time_t; 359 + time_t time(time_t *t) { 360 + time_t now = 1775000000; 361 + if (t) *t = now; 362 + return now; 363 + } 364 + 365 + struct timeval { long tv_sec; long tv_usec; }; 366 + struct timezone { int tz_minuteswest; int tz_dsttime; }; 367 + int gettimeofday(struct timeval *tv, struct timezone *tz) { 368 + if (tv) { tv->tv_sec = 1775000000; tv->tv_usec = 0; } 369 + if (tz) { tz->tz_minuteswest = 0; tz->tz_dsttime = 0; } 370 + return 0; 371 + } 372 + 373 + struct tm { int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, 374 + tm_wday, tm_yday, tm_isdst; }; 375 + static struct tm _dummy_tm; 376 + struct tm *localtime(const time_t *t) { (void)t; return &_dummy_tm; } 377 + 378 + size_t strftime(char *s, size_t max, const char *fmt, const struct tm *tm) { 379 + (void)fmt; (void)tm; 380 + if (max > 0) s[0] = '\0'; 381 + return 0; 382 + } 383 + 384 + /* ---- abort ---- */ 385 + 386 + void abort(void) { for (;;); } 387 + 388 + /* ---- Misc stubs needed by openh264 ---- */ 389 + 390 + int getentropy(void *buf, size_t len) { 391 + unsigned char *p = buf; 392 + if (cb_rand) { 393 + for (size_t i = 0; i < len; i += 8) { 394 + uint64_t r = cb_rand(); 395 + size_t chunk = len - i; 396 + if (chunk > 8) chunk = 8; 397 + memcpy(p + i, &r, chunk); 398 + } 399 + return 0; 400 + } 401 + static unsigned long long seed = 0x12345678DEADBEEF; 402 + for (size_t i = 0; i < len; i++) { 403 + seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; 404 + p[i] = (unsigned char)(seed >> 33); 405 + } 406 + return 0; 407 + } 408 + 409 + unsigned int minilib_lwip_rand(void) { 410 + if (cb_rand) return (unsigned int)cb_rand(); 411 + static unsigned int seed = 0xDEADBEEF; 412 + seed = seed * 1103515245 + 12345; 413 + return seed; 414 + } 415 + 416 + unsigned int sys_now(void) { 417 + if (cb_ticks) return (unsigned int)cb_ticks(); 418 + static unsigned int ticks = 0; 419 + return ticks += 10; 420 + } 421 + 422 + /* ---- Pthread stubs (single-threaded: all create/init fail) ---- */ 423 + 424 + typedef unsigned long pthread_t; 425 + typedef struct { int dummy; } pthread_mutex_t; 426 + typedef struct { int dummy; } pthread_cond_t; 427 + typedef struct { int dummy; } pthread_attr_t; 428 + typedef unsigned int sem_t; 429 + 430 + int pthread_mutex_init(pthread_mutex_t *m, const void *a) { (void)m; (void)a; return 0; } 431 + int pthread_mutex_lock(pthread_mutex_t *m) { (void)m; return 0; } 432 + int pthread_mutex_unlock(pthread_mutex_t *m) { (void)m; return 0; } 433 + int pthread_mutex_destroy(pthread_mutex_t *m) { (void)m; return 0; } 434 + 435 + int pthread_cond_init(pthread_cond_t *c, const void *a) { (void)c; (void)a; return 0; } 436 + int pthread_cond_signal(pthread_cond_t *c) { (void)c; return 0; } 437 + int pthread_cond_broadcast(pthread_cond_t *c) { (void)c; return 0; } 438 + int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m) { (void)c; (void)m; return 0; } 439 + int pthread_cond_destroy(pthread_cond_t *c) { (void)c; return 0; } 440 + 441 + int pthread_attr_init(pthread_attr_t *a) { (void)a; return 0; } 442 + int pthread_attr_setschedpolicy(pthread_attr_t *a, int p) { (void)a; (void)p; return 0; } 443 + int pthread_attr_setschedparam(pthread_attr_t *a, const void *p) { (void)a; (void)p; return 0; } 444 + int pthread_attr_destroy(pthread_attr_t *a) { (void)a; return 0; } 445 + 446 + int pthread_attr_setscope(pthread_attr_t *a, int s) { (void)a; (void)s; return 0; } 447 + 448 + int pthread_create(pthread_t *t, const pthread_attr_t *a, void *(*fn)(void*), void *arg) { 449 + (void)t; (void)a; (void)fn; (void)arg; 450 + return -1; /* fail → decoder falls back to single-threaded */ 451 + } 452 + int pthread_join(pthread_t t, void **r) { (void)t; (void)r; return -1; } 453 + pthread_t pthread_self(void) { return 0; } 454 + 455 + int usleep(unsigned usec) { (void)usec; return 0; } 456 + 457 + int sem_init(sem_t *s, int p, unsigned v) { (void)s; (void)p; (void)v; return 0; } 458 + int sem_destroy(sem_t *s) { (void)s; return 0; } 459 + int sem_post(sem_t *s) { (void)s; return 0; } 460 + int sem_wait(sem_t *s) { (void)s; return 0; } 461 + int sem_timedwait(sem_t *s, const void *t) { (void)s; (void)t; return -1; } 462 + int sem_trywait(sem_t *s) { (void)s; return -1; } 463 + 464 + int pthread_cond_timedwait(pthread_cond_t *c, pthread_mutex_t *m, const void *t) { 465 + (void)c; (void)m; (void)t; return -1; 466 + } 467 + 468 + /* sched stubs */ 469 + typedef struct { unsigned long __bits[16]; } cpu_set_t; 470 + int sched_getaffinity(int pid, size_t sz, cpu_set_t *set) { 471 + (void)pid; (void)sz; 472 + memset(set, 0, sizeof(*set)); 473 + set->__bits[0] = 1; /* claim 1 CPU */ 474 + return 0; 475 + } 476 + int __sched_cpucount(size_t sz, const cpu_set_t *set) { 477 + (void)sz; (void)set; 478 + return 1; 479 + } 480 + 481 + /* ---- C++ runtime stubs ---- */ 482 + 483 + /* operator new/delete — route through our malloc/free */ 484 + void *_Znwm(size_t size) { return malloc(size); } /* operator new(size_t) */ 485 + void *_Znam(size_t size) { return malloc(size); } /* operator new[](size_t) */ 486 + void _ZdlPv(void *ptr) { free(ptr); } /* operator delete(void*) */ 487 + void _ZdaPv(void *ptr) { free(ptr); } /* operator delete[](void*) */ 488 + void _ZdlPvm(void *ptr, size_t sz) { (void)sz; free(ptr); } /* operator delete(void*, size_t) */ 489 + void _ZdaPvm(void *ptr, size_t sz) { (void)sz; free(ptr); } /* operator delete[](void*, size_t) */ 490 + 491 + /* Static initialization guards (single-threaded, no races) */ 492 + int __cxa_guard_acquire(int64_t *guard) { 493 + if (*guard) return 0; /* already initialized */ 494 + return 1; /* needs initialization */ 495 + } 496 + void __cxa_guard_release(int64_t *guard) { *guard = 1; } 497 + void __cxa_guard_abort(int64_t *guard) { (void)guard; } 498 + 499 + /* Static destructor registration — no-op (we never exit cleanly) */ 500 + int __cxa_atexit(void (*fn)(void *), void *arg, void *dso) { 501 + (void)fn; (void)arg; (void)dso; 502 + return 0; 503 + } 504 + 505 + /* Pure virtual call handler */ 506 + void __cxa_pure_virtual(void) { for (;;); } 507 + 508 + /* Stack protector — no-op */ 509 + unsigned long __stack_chk_guard = 0xDEADBEEF; 510 + void __stack_chk_fail(void) { for (;;); }
+169
media/openh264-no-float.patch
··· 1 + diff --git a/codec/common/src/utils.cpp b/codec/common/src/utils.cpp 2 + index fc0fbf90..ffc78f28 100644 3 + --- a/codec/common/src/utils.cpp 4 + +++ b/codec/common/src/utils.cpp 5 + @@ -40,12 +40,14 @@ 6 + #include "utils.h" 7 + #include "crt_util_safe_x.h" // Safe CRT routines like utils for cross platforms 8 + #include "codec_app_def.h" 9 + +#ifndef VODBROWSER_NO_FLOAT 10 + float WelsCalcPsnr (const void* kpTarPic, 11 + const int32_t kiTarStride, 12 + const void* kpRefPic, 13 + const int32_t kiRefStride, 14 + const int32_t kiWidth, 15 + const int32_t kiHeight); 16 + +#endif 17 + 18 + 19 + void WelsLog (SLogContext* logCtx, int32_t iLevel, const char* kpFmt, ...) { 20 + @@ -75,8 +77,8 @@ void WelsLog (SLogContext* logCtx, int32_t iLevel, const char* kpFmt, ...) { 21 + } 22 + 23 + #ifndef CALC_PSNR 24 + -#define CONST_FACTOR_PSNR (10.0 / log(10.0)) // for good computation 25 + -#define CALC_PSNR(w, h, s) ((float)(CONST_FACTOR_PSNR * log( 65025.0 * w * h / s ))) 26 + +// Stubbed — no floating point on TempleOS (SSE disabled) 27 + +#define CALC_PSNR(w, h, s) (0) 28 + #endif//CALC_PSNR 29 + 30 + /* 31 + @@ -98,29 +100,16 @@ void WelsLog (SLogContext* logCtx, int32_t iLevel, const char* kpFmt, ...) { 32 + * \note N/A 33 + ************************************************************************************* 34 + */ 35 + +#ifndef VODBROWSER_NO_FLOAT 36 + float WelsCalcPsnr (const void* kpTarPic, 37 + const int32_t kiTarStride, 38 + const void* kpRefPic, 39 + const int32_t kiRefStride, 40 + const int32_t kiWidth, 41 + const int32_t kiHeight) { 42 + - int64_t iSqe = 0; 43 + - int32_t x, y; 44 + - uint8_t* pTar = (uint8_t*)kpTarPic; 45 + - uint8_t* pRef = (uint8_t*)kpRefPic; 46 + - 47 + - if (NULL == pTar || NULL == pRef) 48 + - return (-1.0f); 49 + - 50 + - for (y = 0; y < kiHeight; ++ y) { // OPTable !! 51 + - for (x = 0; x < kiWidth; ++ x) { 52 + - const int32_t kiT = pTar[y * kiTarStride + x] - pRef[y * kiRefStride + x]; 53 + - iSqe += kiT * kiT; 54 + - } 55 + - } 56 + - if (0 == iSqe) { 57 + - return (99.99f); 58 + - } 59 + - return CALC_PSNR (kiWidth, kiHeight, iSqe); 60 + + (void)kpTarPic; (void)kiTarStride; (void)kpRefPic; 61 + + (void)kiRefStride; (void)kiWidth; (void)kiHeight; 62 + + return 0; 63 + } 64 + +#endif 65 + 66 + diff --git a/codec/decoder/core/src/deblocking.cpp b/codec/decoder/core/src/deblocking.cpp 67 + index 1837c76d..cf965600 100644 68 + --- a/codec/decoder/core/src/deblocking.cpp 69 + +++ b/codec/decoder/core/src/deblocking.cpp 70 + @@ -1213,6 +1213,10 @@ void WelsDeblockingMb (PDqLayer pCurDqLayer, PDeblockingFilter pFilter, int32_t 71 + * \return NONE 72 + */ 73 + void WelsDeblockingFilterSlice (PWelsDecoderContext pCtx, PDeblockingFilterMbFunc pDeblockMb) { 74 + +#ifdef VODBROWSER_NO_DEBLOCK 75 + + (void)pCtx; (void)pDeblockMb; 76 + + return; 77 + +#else 78 + PDqLayer pCurDqLayer = pCtx->pCurDqLayer; 79 + PSliceHeaderExt pSliceHeaderExt = &pCurDqLayer->sLayerInfo.sSliceInLayer.sSliceHeaderExt; 80 + int32_t iMbWidth = pCurDqLayer->iMbWidth; 81 + @@ -1275,6 +1279,7 @@ void WelsDeblockingFilterSlice (PWelsDecoderContext pCtx, PDeblockingFilterMbFun 82 + pCurDqLayer->iMbXyIndex = iNextMbXyIndex; 83 + } while (1); 84 + } 85 + +#endif // VODBROWSER_NO_DEBLOCK 86 + } 87 + 88 + /*! 89 + @@ -1320,11 +1325,15 @@ void WelsDeblockingInitFilter (PWelsDecoderContext pCtx, SDeblockingFilter& pFil 90 + */ 91 + void WelsDeblockingFilterMB (PDqLayer pCurDqLayer, SDeblockingFilter& pFilter, int32_t& iFilterIdc, 92 + PDeblockingFilterMbFunc pDeblockMb) { 93 + +#ifdef VODBROWSER_NO_DEBLOCK 94 + + (void)pCurDqLayer; (void)pFilter; (void)iFilterIdc; (void)pDeblockMb; 95 + +#else 96 + /* macroblock deblocking */ 97 + if (0 == iFilterIdc || 2 == iFilterIdc) { 98 + int32_t iBoundryFlag = DeblockingAvailableNoInterlayer (pCurDqLayer, iFilterIdc); 99 + pDeblockMb (pCurDqLayer, &pFilter, iBoundryFlag); 100 + } 101 + +#endif 102 + } 103 + /*! 104 + * \brief deblocking module initialize 105 + diff --git a/codec/decoder/core/src/decoder_core.cpp b/codec/decoder/core/src/decoder_core.cpp 106 + index dd5d5888..d3d1965b 100644 107 + --- a/codec/decoder/core/src/decoder_core.cpp 108 + +++ b/codec/decoder/core/src/decoder_core.cpp 109 + @@ -1253,8 +1253,12 @@ int32_t ParseSliceHeaderSyntaxs (PWelsDecoderContext pCtx, PBitStringAux pBs, co 110 + bSgChangeCycleInvolved = (bSgChangeCycleInvolved && (uiQualityId == BASE_QUALITY_ID)); 111 + if (bSgChangeCycleInvolved) { 112 + if (pPps->uiSliceGroupChangeRate > 0) { 113 + - const int32_t kiNumBits = (int32_t)WELS_CEIL (log (static_cast<double> (1 + pPps->uiPicSizeInMapUnits / 114 + - pPps->uiSliceGroupChangeRate))); 115 + + // Integer-only ceil(log(x)) — avoids SSE (no FPU on TempleOS) 116 + + // Computes ceil(ln(1 + size/rate)) using a precomputed e^n table 117 + + uint32_t _fmo_x = 1 + pPps->uiPicSizeInMapUnits / pPps->uiSliceGroupChangeRate; 118 + + int32_t kiNumBits = 0; 119 + + { static const uint32_t _e[] = {1,3,8,21,55,149,404,1097,2981,8104,22027,59875,162755,442414,0xFFFFFFFF}; 120 + + while (_e[kiNumBits] < _fmo_x && _e[kiNumBits] != 0xFFFFFFFF) kiNumBits++; } 121 + WELS_READ_VERIFY (BsGetBits (pBs, kiNumBits, &uiCode)); //lice_group_change_cycle 122 + pSliceHead->iSliceGroupChangeCycle = uiCode; 123 + } else 124 + diff --git a/codec/decoder/plus/src/welsDecoderExt.cpp b/codec/decoder/plus/src/welsDecoderExt.cpp 125 + index 08e037a3..07ab7ab9 100644 126 + --- a/codec/decoder/plus/src/welsDecoderExt.cpp 127 + +++ b/codec/decoder/plus/src/welsDecoderExt.cpp 128 + @@ -642,11 +642,9 @@ long CWelsDecoder::GetOption (DECODER_OPTION eOptID, void* pOption) { 129 + memcpy (pDecoderStatistics, pDecContext->pDecoderStatistics, sizeof (SDecoderStatistics)); 130 + 131 + if (pDecContext->pDecoderStatistics->uiDecodedFrameCount != 0) { //not original status 132 + - pDecoderStatistics->fAverageFrameSpeedInMs = (float) (pDecContext->dDecTime) / 133 + - (pDecContext->pDecoderStatistics->uiDecodedFrameCount); 134 + - pDecoderStatistics->fActualAverageFrameSpeedInMs = (float) (pDecContext->dDecTime) / 135 + - (pDecContext->pDecoderStatistics->uiDecodedFrameCount + pDecContext->pDecoderStatistics->uiFreezingIDRNum + 136 + - pDecContext->pDecoderStatistics->uiFreezingNonIDRNum); 137 + + // Skip float stats — no SSE on TempleOS 138 + + pDecoderStatistics->fAverageFrameSpeedInMs = 0; 139 + + pDecoderStatistics->fActualAverageFrameSpeedInMs = 0; 140 + } 141 + return cmResultSuccess; 142 + } else if (eOptID == DECODER_OPTION_STATISTICS_LOG_INTERVAL) { 143 + @@ -879,7 +877,7 @@ DECODING_STATE CWelsDecoder::DecodeFrame2WithCtx (PWelsDecoderContext pDecContex 144 + pDecContext->pDecoderStatistics->uiAvgEcPropRatio / pDecContext->pDecoderStatistics->uiEcFrameNum; 145 + } 146 + iEnd = WelsTime(); 147 + - pDecContext->dDecTime += (iEnd - iStart) / 1e3; 148 + + pDecContext->dDecTime += (iEnd - iStart) / 1000; 149 + 150 + OutputStatisticsLog (*pDecContext->pDecoderStatistics); 151 + if (GetThreadCount (pDecContext) >= 1) { 152 + @@ -904,7 +902,7 @@ DECODING_STATE CWelsDecoder::DecodeFrame2WithCtx (PWelsDecoderContext pDecContex 153 + OutputStatisticsLog (*pDecContext->pDecoderStatistics); 154 + } 155 + iEnd = WelsTime(); 156 + - pDecContext->dDecTime += (iEnd - iStart) / 1e3; 157 + + pDecContext->dDecTime += (iEnd - iStart) / 1000; 158 + 159 + if (GetThreadCount (pDecContext) >= 1) { 160 + BufferingReadyPicture (pDecContext, ppDst, pDstInfo); 161 + @@ -1252,7 +1250,7 @@ DECODING_STATE CWelsDecoder::DecodeParser (const unsigned char* kpSrc, const int 162 + pDecContext->bPrintFrameErrorTraceFlag = false; 163 + } 164 + iEnd = WelsTime(); 165 + - pDecContext->dDecTime += (iEnd - iStart) / 1e3; 166 + + pDecContext->dDecTime += (iEnd - iStart) / 1000; 167 + return (DECODING_STATE)pDecContext->iErrorCode; 168 + } 169 +
+142
net/Makefile
··· 1 + ROOT = .. 2 + ELF_DIR = $(ROOT)/elf 3 + 4 + # Libraries 5 + BEARSSL_DIR = $(ROOT)/lib/bearssl 6 + PHTTP_DIR = $(ROOT)/lib/picohttpparser 7 + JSMN_DIR = $(ROOT)/lib/jsmn 8 + LWIP_DIR = $(ROOT)/lib/lwip 9 + 10 + # Cross-compiler for the TempleOS ELF (x86_64-linux-musl target, 11 + # freestanding, no SSE/MMX/float — TempleOS V5.03 has no CR4.OSFXSR). 12 + ZIG = zig 13 + ZIGCC = $(ZIG) cc -target x86_64-linux-musl 14 + LLD = $(ZIG) ld.lld 15 + XCFLAGS = -fPIC -fno-stack-protector -fno-sanitize=all -fwrapv -O2 \ 16 + -mno-sse -mno-sse2 -mno-mmx -msoft-float -mno-red-zone 17 + 18 + # Include paths shared by every compile unit 19 + INCLUDES = -I. -Iarch \ 20 + -I$(BEARSSL_DIR)/inc \ 21 + -I$(PHTTP_DIR) \ 22 + -I$(JSMN_DIR) \ 23 + -I$(LWIP_DIR)/src/include 24 + 25 + # lwIP core sources needed for TCP + DNS + DHCP 26 + LWIP_SRCS = $(LWIP_DIR)/src/core/def.c \ 27 + $(LWIP_DIR)/src/core/dns.c \ 28 + $(LWIP_DIR)/src/core/inet_chksum.c \ 29 + $(LWIP_DIR)/src/core/init.c \ 30 + $(LWIP_DIR)/src/core/ip.c \ 31 + $(LWIP_DIR)/src/core/mem.c \ 32 + $(LWIP_DIR)/src/core/memp.c \ 33 + $(LWIP_DIR)/src/core/netif.c \ 34 + $(LWIP_DIR)/src/core/pbuf.c \ 35 + $(LWIP_DIR)/src/core/tcp.c \ 36 + $(LWIP_DIR)/src/core/tcp_in.c \ 37 + $(LWIP_DIR)/src/core/tcp_out.c \ 38 + $(LWIP_DIR)/src/core/timeouts.c \ 39 + $(LWIP_DIR)/src/core/udp.c \ 40 + $(LWIP_DIR)/src/core/ipv4/acd.c \ 41 + $(LWIP_DIR)/src/core/ipv4/dhcp.c \ 42 + $(LWIP_DIR)/src/core/ipv4/etharp.c \ 43 + $(LWIP_DIR)/src/core/ipv4/icmp.c \ 44 + $(LWIP_DIR)/src/core/ipv4/ip4.c \ 45 + $(LWIP_DIR)/src/core/ipv4/ip4_addr.c \ 46 + $(LWIP_DIR)/src/core/ipv4/ip4_frag.c \ 47 + $(LWIP_DIR)/src/netif/ethernet.c 48 + 49 + # ---- Cross-compiled ELF for TempleOS ---- 50 + 51 + XBUILD = build_elf 52 + XOBJS = $(XBUILD)/net_api.o $(XBUILD)/net_entry.o $(XBUILD)/tos_netif.o \ 53 + $(XBUILD)/minilib.o $(XBUILD)/json_wrap.o $(XBUILD)/picohttpparser.o \ 54 + $(XBUILD)/bearssl_stubs.o \ 55 + $(patsubst $(LWIP_DIR)/%.c,$(XBUILD)/lwip_%.o,$(LWIP_SRCS)) 56 + 57 + # BearSSL objects — exclude x86-specific SSE/AES-NI/PCLMUL implementations 58 + # BearSSL will use the pure-C fallbacks (aes_ct, ghash_ctmul, chacha20_ct) 59 + BEARSSL_CSRC = $(filter-out %pclmul.c %x86ni.c %x86ni_cbcdec.c %x86ni_cbcenc.c %x86ni_ctr.c %x86ni_ctrcbc.c %sse2.c %pwr8.c %pwr8_cbcdec.c %pwr8_cbcenc.c %pwr8_ctr.c %pwr8_ctrcbc.c, \ 60 + $(wildcard $(BEARSSL_DIR)/src/*.c) \ 61 + $(wildcard $(BEARSSL_DIR)/src/*/*.c)) 62 + BEARSSL_XOBJS = $(patsubst $(BEARSSL_DIR)/src/%.c,$(XBUILD)/bearssl_%.o,$(BEARSSL_CSRC)) 63 + 64 + .PHONY: all elf clean 65 + 66 + all: elf 67 + 68 + elf: Net.prg 69 + 70 + # Our sources 71 + # lwipopts.h is listed as a dep on every TU that #includes lwIP headers, 72 + # because lwIP's opt.h splices it into every compilation unit. Without 73 + # this, bumping TCP_WND / MEMP_NUM_* / etc. in lwipopts.h silently fails 74 + # to recompile and Net.prg ships with stale tuning constants. 75 + $(XBUILD)/net_api.o: net_api.c net_api.h tos_netif.h lwipopts.h | $(XBUILD) 76 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 77 + 78 + $(XBUILD)/net_entry.o: net_entry.c net_api.h | $(XBUILD) 79 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 80 + 81 + $(XBUILD)/tos_netif.o: tos_netif.c tos_netif.h lwipopts.h | $(XBUILD) 82 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 83 + 84 + $(XBUILD)/minilib.o: minilib.c | $(XBUILD) 85 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 86 + 87 + $(XBUILD)/bearssl_stubs.o: bearssl_stubs.c | $(XBUILD) 88 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 89 + 90 + # Vendored single-file libs 91 + $(XBUILD)/picohttpparser.o: $(PHTTP_DIR)/picohttpparser.c | $(XBUILD) 92 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 93 + 94 + # json_wrap + jsmn (header-only, included via JSMN_STATIC) 95 + $(XBUILD)/json_wrap.o: json_wrap.c json_wrap.h | $(XBUILD) 96 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 97 + 98 + # lwIP sources 99 + # 100 + # lwipopts.h is a build-configuration header that lwIP splices into 101 + # every TU via opt.h. If Make doesn't track it, bumping constants 102 + # like TCP_WND silently has no effect: the .o files stay cached with 103 + # the old values and only the binary's LINK step re-runs, producing 104 + # an identical binary. Making the rule depend on lwipopts.h forces a 105 + # rebuild of every lwIP core file whenever tuning changes. 106 + $(XBUILD)/lwip_%.o: $(LWIP_DIR)/%.c lwipopts.h | $(XBUILD) 107 + @mkdir -p $(dir $@) 108 + $(ZIGCC) $(XCFLAGS) $(INCLUDES) -c -o $@ $< 109 + 110 + # BearSSL sources — disable all x86/POWER8 intrinsics 111 + BEARSSL_DEFINES = -DBR_AES_X86NI=0 -DBR_SSE2=0 -DBR_POWER8=0 \ 112 + -DBR_LOMUL=1 -DBR_USE_GETENTROPY=1 -DBR_USE_URANDOM=0 -DBR_RDRAND=0 113 + $(XBUILD)/bearssl_%.o: $(BEARSSL_DIR)/src/%.c | $(XBUILD) 114 + @mkdir -p $(dir $@) 115 + $(ZIGCC) $(XCFLAGS) $(BEARSSL_DEFINES) -I$(BEARSSL_DIR)/inc -I$(BEARSSL_DIR)/src -c -o $@ $< 116 + 117 + # Setup.s (ELF entry) 118 + $(XBUILD)/setup.o: $(ELF_DIR)/setup.s | $(XBUILD) 119 + $(ZIGCC) -c -fPIC -fno-stack-protector -nostdlib -o $@ $< 120 + 121 + # Link 122 + # Link with zig's lld — minilib.c provides all libc functions we need 123 + $(XBUILD)/unoffset.elf: $(XBUILD)/setup.o $(XOBJS) $(BEARSSL_XOBJS) 124 + $(LLD) -T $(ELF_DIR)/unoffset.x -o $@ $^ 125 + 126 + $(XBUILD)/offset.elf: $(XBUILD)/setup.o $(XOBJS) $(BEARSSL_XOBJS) 127 + $(LLD) -T $(ELF_DIR)/offset.x -o $@ $^ 128 + 129 + $(XBUILD)/unoffset.bin: $(XBUILD)/unoffset.elf 130 + python3 $(ELF_DIR)/elf2bin.py $< $@ 131 + 132 + $(XBUILD)/offset.bin: $(XBUILD)/offset.elf 133 + python3 $(ELF_DIR)/elf2bin.py $< $@ 134 + 135 + Net.prg: $(XBUILD)/unoffset.bin $(XBUILD)/offset.bin 136 + cd $(XBUILD) && python3 ../../elf/make_program.py ../../net/Net.prg 137 + 138 + $(XBUILD): 139 + mkdir -p $(XBUILD) 140 + 141 + clean: 142 + rm -rf $(XBUILD) Net.prg
+45
net/arch/cc.h
··· 1 + /* 2 + * cc.h — lwIP compiler/platform abstraction for VodBrowser (freestanding) 3 + */ 4 + #ifndef LWIP_ARCH_CC_H 5 + #define LWIP_ARCH_CC_H 6 + 7 + #include <stdint.h> 8 + #include <stddef.h> 9 + 10 + /* Types */ 11 + typedef uint8_t u8_t; 12 + typedef int8_t s8_t; 13 + typedef uint16_t u16_t; 14 + typedef int16_t s16_t; 15 + typedef uint32_t u32_t; 16 + typedef int32_t s32_t; 17 + typedef uintptr_t mem_ptr_t; 18 + 19 + /* Printf format macros */ 20 + #define U16_F "u" 21 + #define S16_F "d" 22 + #define X16_F "x" 23 + #define U32_F "u" 24 + #define S32_F "d" 25 + #define X32_F "x" 26 + 27 + /* Byte order — provided by musl's endian.h */ 28 + #include <endian.h> 29 + 30 + /* Structure packing */ 31 + #define PACK_STRUCT_FIELD(x) x 32 + #define PACK_STRUCT_STRUCT __attribute__((packed)) 33 + #define PACK_STRUCT_BEGIN 34 + #define PACK_STRUCT_END 35 + 36 + /* Platform-specific diagnostic output */ 37 + extern void tos_print(const char *s); 38 + #define LWIP_PLATFORM_DIAG(x) do { /* no debug output */ } while(0) 39 + #define LWIP_PLATFORM_ASSERT(x) do { /* no assert */ } while(0) 40 + 41 + /* Random number — use minilib's RNG (calls TempleOS RandU64 when available) */ 42 + extern unsigned int minilib_lwip_rand(void); 43 + #define LWIP_RAND() minilib_lwip_rand() 44 + 45 + #endif
+16
net/arch/sys_arch.h
··· 1 + /* 2 + * sys_arch.h — lwIP system abstraction for NO_SYS=1 (no OS) 3 + * Minimal stubs — lwIP in NO_SYS mode doesn't use most of this. 4 + */ 5 + #ifndef LWIP_ARCH_SYS_ARCH_H 6 + #define LWIP_ARCH_SYS_ARCH_H 7 + 8 + #define SYS_MBOX_NULL NULL 9 + #define SYS_SEM_NULL NULL 10 + 11 + typedef void * sys_sem_t; 12 + typedef void * sys_mbox_t; 13 + typedef void * sys_thread_t; 14 + typedef int sys_prot_t; 15 + 16 + #endif
+27
net/bearssl_stubs.c
··· 1 + /* 2 + * bearssl_stubs.c — Stub out x86-specific BearSSL vtables 3 + * 4 + * BearSSL's TLS engine checks for x86 AES-NI, PCLMUL, SSE2, POWER8 5 + * at runtime and uses hardware-accelerated implementations if available. 6 + * On TempleOS, SSE is not enabled, so these must always return NULL. 7 + */ 8 + #include <stddef.h> 9 + 10 + /* AES x86ni stubs */ 11 + void *br_aes_x86ni_cbcenc_get_vtable(void) { return NULL; } 12 + void *br_aes_x86ni_cbcdec_get_vtable(void) { return NULL; } 13 + void *br_aes_x86ni_ctr_get_vtable(void) { return NULL; } 14 + void *br_aes_x86ni_ctrcbc_get_vtable(void) { return NULL; } 15 + 16 + /* GHASH pclmul stub */ 17 + void *br_ghash_pclmul_get(void) { return NULL; } 18 + 19 + /* ChaCha20 SSE2 stub */ 20 + void *br_chacha20_sse2_get(void) { return NULL; } 21 + 22 + /* POWER8 stubs */ 23 + void *br_aes_pwr8_cbcenc_get_vtable(void) { return NULL; } 24 + void *br_aes_pwr8_cbcdec_get_vtable(void) { return NULL; } 25 + void *br_aes_pwr8_ctr_get_vtable(void) { return NULL; } 26 + void *br_aes_pwr8_ctrcbc_get_vtable(void) { return NULL; } 27 + void *br_ghash_pwr8_get(void) { return NULL; }
+179
net/json_wrap.c
··· 1 + /* 2 + * json_wrap.c — jsmn wrapper implementation 3 + * 4 + * Provides tree-like JSON navigation over jsmn's flat token array. 5 + * No floating point, no SSE, fully freestanding. 6 + */ 7 + #include "json_wrap.h" 8 + #include <string.h> 9 + #include <stdlib.h> 10 + 11 + /* Compare a jsmn token's string value against a C string */ 12 + static int tok_eq(const char *js, const jsmntok_t *tok, const char *s) { 13 + size_t len = (size_t)(tok->end - tok->start); 14 + return (tok->type == JSMN_STRING && 15 + strlen(s) == len && 16 + memcmp(js + tok->start, s, len) == 0); 17 + } 18 + 19 + /* Advance past a token and everything nested inside it. jsmn sets 20 + * .end on objects, arrays, strings and primitives to the byte offset 21 + * in the source just after the token ends, so any descendant is 22 + * entirely inside [.start, .end). We walk forward while successive 23 + * tokens begin before idx.end and stop as soon as we cross the 24 + * boundary. 25 + * 26 + * The earlier implementation tried to count children and recurse 27 + * `size` times per token, with a `*= 2` tweak for objects. That was 28 + * wrong: jsmn records each object key STRING with size=1 (pointing at 29 + * its value), so a single recursive step already consumes both key and 30 + * value. Multiplying the outer loop count by 2 caused tok_skip to walk 31 + * past the end of the object into unrelated tokens, which in turn made 32 + * json_arr_idx land inside the wrong record — that's the root cause of 33 + * the "only the first video title shows" bug. */ 34 + static int tok_skip(const jsmntok_t *tokens, int idx, int ntokens) { 35 + if (idx >= ntokens) return ntokens; 36 + int end = tokens[idx].end; 37 + int i = idx + 1; 38 + while (i < ntokens && tokens[i].start < end) i++; 39 + return i; 40 + } 41 + 42 + /* Find a key in an object token. Returns token index of the VALUE, or -1. */ 43 + static int obj_find(json_doc_t *doc, int obj_idx, const char *key) { 44 + if (obj_idx >= doc->ntokens) return -1; 45 + const jsmntok_t *obj = &doc->tokens[obj_idx]; 46 + if (obj->type != JSMN_OBJECT) return -1; 47 + 48 + int i = obj_idx + 1; 49 + for (int k = 0; k < obj->size && i + 1 < doc->ntokens; k++) { 50 + if (tok_eq(doc->js, &doc->tokens[i], key)) 51 + return i + 1; /* return the value token (after the key) */ 52 + i = tok_skip(doc->tokens, i + 1, doc->ntokens); /* skip value */ 53 + } 54 + return -1; 55 + } 56 + 57 + /* Allocate a handle pointing to a specific token */ 58 + static json_handle_t *make_handle(json_doc_t *doc, int idx) { 59 + json_handle_t *h = malloc(sizeof(json_handle_t)); 60 + if (!h) return NULL; 61 + h->doc = doc; 62 + h->idx = idx; 63 + return h; 64 + } 65 + 66 + /* ---- Public API ---- */ 67 + 68 + json_handle_t *json_parse(const char *data, size_t len) { 69 + json_doc_t *doc = malloc(sizeof(json_doc_t)); 70 + if (!doc) return NULL; 71 + 72 + /* Copy the JSON string so it stays alive */ 73 + char *js_copy = malloc(len + 1); 74 + if (!js_copy) { free(doc); return NULL; } 75 + memcpy(js_copy, data, len); 76 + js_copy[len] = '\0'; 77 + doc->js = js_copy; 78 + doc->js_len = len; 79 + 80 + jsmn_parser parser; 81 + jsmn_init(&parser); 82 + doc->ntokens = jsmn_parse(&parser, doc->js, len, 83 + doc->tokens, JSON_MAX_TOKENS); 84 + if (doc->ntokens < 0) { 85 + free(js_copy); 86 + free(doc); 87 + return NULL; 88 + } 89 + 90 + return make_handle(doc, 0); 91 + } 92 + 93 + void json_free(json_handle_t *h) { 94 + if (!h) return; 95 + /* Only free the doc if this is the root handle (idx == 0). 96 + * Sub-handles share the doc and shouldn't free it. */ 97 + if (h->idx == 0 && h->doc) { 98 + free((void *)h->doc->js); 99 + free(h->doc); 100 + } 101 + free(h); 102 + } 103 + 104 + const char *json_str(json_handle_t *h, const char *key) { 105 + if (!h || !h->doc) return NULL; 106 + int vi = obj_find(h->doc, h->idx, key); 107 + if (vi < 0) return NULL; 108 + const jsmntok_t *t = &h->doc->tokens[vi]; 109 + if (t->type != JSMN_STRING) return NULL; 110 + /* Copy string — avoids corrupting the JSON buffer */ 111 + size_t len = (size_t)(t->end - t->start); 112 + char *copy = malloc(len + 1); 113 + if (!copy) return NULL; 114 + memcpy(copy, h->doc->js + t->start, len); 115 + copy[len] = '\0'; 116 + return copy; 117 + } 118 + 119 + json_handle_t *json_obj(json_handle_t *h, const char *key) { 120 + if (!h || !h->doc) return NULL; 121 + int vi = obj_find(h->doc, h->idx, key); 122 + if (vi < 0) return NULL; 123 + if (h->doc->tokens[vi].type != JSMN_OBJECT) return NULL; 124 + return make_handle(h->doc, vi); 125 + } 126 + 127 + json_handle_t *json_arr(json_handle_t *h, const char *key) { 128 + if (!h || !h->doc) return NULL; 129 + int vi = obj_find(h->doc, h->idx, key); 130 + if (vi < 0) return NULL; 131 + if (h->doc->tokens[vi].type != JSMN_ARRAY) return NULL; 132 + return make_handle(h->doc, vi); 133 + } 134 + 135 + int json_arr_len(json_handle_t *h) { 136 + if (!h || !h->doc || h->idx >= h->doc->ntokens) return 0; 137 + const jsmntok_t *t = &h->doc->tokens[h->idx]; 138 + if (t->type != JSMN_ARRAY) return 0; 139 + return t->size; 140 + } 141 + 142 + json_handle_t *json_arr_idx(json_handle_t *h, int idx) { 143 + if (!h || !h->doc || h->idx >= h->doc->ntokens) return NULL; 144 + const jsmntok_t *arr = &h->doc->tokens[h->idx]; 145 + if (arr->type != JSMN_ARRAY) return NULL; 146 + if (idx >= arr->size) return NULL; 147 + 148 + int i = h->idx + 1; 149 + for (int c = 0; c < idx && i < h->doc->ntokens; c++) 150 + i = tok_skip(h->doc->tokens, i, h->doc->ntokens); 151 + if (i >= h->doc->ntokens) return NULL; 152 + return make_handle(h->doc, i); 153 + } 154 + 155 + int json_is_str(json_handle_t *h) { 156 + return h && h->doc && h->idx < h->doc->ntokens && 157 + h->doc->tokens[h->idx].type == JSMN_STRING; 158 + } 159 + 160 + int json_is_arr(json_handle_t *h) { 161 + return h && h->doc && h->idx < h->doc->ntokens && 162 + h->doc->tokens[h->idx].type == JSMN_ARRAY; 163 + } 164 + 165 + int json_is_obj(json_handle_t *h) { 166 + return h && h->doc && h->idx < h->doc->ntokens && 167 + h->doc->tokens[h->idx].type == JSMN_OBJECT; 168 + } 169 + 170 + const char *json_strval(json_handle_t *h) { 171 + if (!json_is_str(h)) return NULL; 172 + const jsmntok_t *t = &h->doc->tokens[h->idx]; 173 + size_t len = (size_t)(t->end - t->start); 174 + char *copy = malloc(len + 1); 175 + if (!copy) return NULL; 176 + memcpy(copy, h->doc->js + t->start, len); 177 + copy[len] = '\0'; 178 + return copy; 179 + }
+63
net/json_wrap.h
··· 1 + /* 2 + * json_wrap.h — Thin wrapper over jsmn for net_api's JSON interface 3 + * 4 + * jsmn is a flat token array. This wrapper provides tree-like navigation 5 + * by key name, matching the net_api.h JSON interface. 6 + */ 7 + #ifndef JSON_WRAP_H 8 + #define JSON_WRAP_H 9 + 10 + #include <stddef.h> 11 + 12 + #define JSMN_STATIC 13 + #include "jsmn.h" 14 + 15 + /* Upper bound on tokens jsmn will emit for a single parsed document. 16 + * ATProto listRecords responses with nested blob refs (each blob ref 17 + * carries $type, $link CID, mimeType, size, plus the enclosing value 18 + * object) easily push 6 records past 2048 tokens, which would cause 19 + * jsmn to return an error and json_parse() to fail. 16384 is ~10x 20 + * headroom at 320KB per json_doc_t, still trivially affordable. */ 21 + #define JSON_MAX_TOKENS 16384 22 + 23 + typedef struct { 24 + const char *js; /* original JSON string (kept alive) */ 25 + size_t js_len; 26 + jsmntok_t tokens[JSON_MAX_TOKENS]; 27 + int ntokens; 28 + } json_doc_t; 29 + 30 + /* A handle is a doc pointer + token index packed together */ 31 + typedef struct { 32 + json_doc_t *doc; 33 + int idx; /* token index within doc->tokens */ 34 + } json_handle_t; 35 + 36 + /* Parse JSON. Caller must free with json_free(). */ 37 + json_handle_t *json_parse(const char *data, size_t len); 38 + void json_free(json_handle_t *h); 39 + 40 + /* Get string value of key in object. Returns NULL if not found. */ 41 + const char *json_str(json_handle_t *h, const char *key); 42 + 43 + /* Get nested object by key. Returns NULL if not found. */ 44 + json_handle_t *json_obj(json_handle_t *h, const char *key); 45 + 46 + /* Get array by key. Returns NULL if not found. */ 47 + json_handle_t *json_arr(json_handle_t *h, const char *key); 48 + 49 + /* Get array length. */ 50 + int json_arr_len(json_handle_t *h); 51 + 52 + /* Get array item by index. */ 53 + json_handle_t *json_arr_idx(json_handle_t *h, int idx); 54 + 55 + /* Type checks */ 56 + int json_is_str(json_handle_t *h); 57 + int json_is_arr(json_handle_t *h); 58 + int json_is_obj(json_handle_t *h); 59 + 60 + /* Get raw string value (for string tokens). Returns NULL-terminated copy. */ 61 + const char *json_strval(json_handle_t *h); 62 + 63 + #endif
+103
net/lwipopts.h
··· 1 + /* 2 + * lwipopts.h — lwIP configuration for VodBrowser 3 + * 4 + * Minimal config: single-threaded (NO_SYS=1), POSIX-compat sockets, 5 + * TCP + DNS. No DHCP (we'll set IP statically from the NIC driver). 6 + */ 7 + #ifndef LWIPOPTS_H 8 + #define LWIPOPTS_H 9 + 10 + /* No OS / single-threaded mode */ 11 + #define NO_SYS 1 12 + #define SYS_LIGHTWEIGHT_PROT 0 13 + #define LWIP_NETCONN 0 14 + #define LWIP_SOCKET 0 /* We'll use the raw API, not socket API */ 15 + 16 + /* Memory */ 17 + #define MEM_SIZE (768 * 1024) /* 768KB heap — holds full TCP window + overhead */ 18 + #define MEM_ALIGNMENT 8 19 + /* PBUF / TCP segment pools sized so a full TCP_WND can be in flight. 20 + * TCP_WND/MSS = 64 segments in flight, so MEMP_NUM_TCP_SEG ≥ 64 and 21 + * MEMP_NUM_PBUF ≥ 64 are required. Doubled for headroom. */ 22 + #define MEMP_NUM_PBUF 128 23 + /* TCP_PCB bumped from 8 → 32 because TIME_WAIT entries count against 24 + * this pool for ~2MSL (default ~2 minutes in lwIP). Even when 25 + * HTTP/1.1 keep-alive is working, the HLS pipeline still opens at 26 + * least 3 new connections (master playlist, sub playlist, init seg) 27 + * plus any cross-host redirects, and an 8-slot pool is too tight. */ 28 + #define MEMP_NUM_TCP_PCB 32 29 + #define MEMP_NUM_TCP_SEG 128 30 + #define PBUF_POOL_SIZE 128 31 + #define PBUF_POOL_BUFSIZE 1536 32 + 33 + /* TCP 34 + * 35 + * TCP_WND bumped 8*MSS → 64*MSS (~93 KB). Rationale: 36 + * 37 + * throughput ≤ TCP_WND / RTT (bandwidth-delay product) 38 + * 39 + * The old 11 KB window capped us at ~2.3 Mbps against a CDN with 40 + * ~40 ms RTT — exactly the sustained throughput we were observing 41 + * in HLS fetch logs (~2200-2400 kbps on every segment). A 93 KB 42 + * window lifts that ceiling to ~18 Mbps at 40 ms RTT, or much 43 + * higher at the sub-10 ms RTTs typical on host-only VM networks. 44 + * Coordinated with Nic.HC which now has a 64-slot RX ring (96 KB 45 + * hardware buffer) so we don't just shift the bottleneck to the 46 + * NIC dropping frames between NetPoll calls. 47 + * 48 + * RFC 1323 window scaling is enabled because TCP_WND (93 KB) is 49 + * larger than the 65535-byte max of the plain TCP header window 50 + * field. TCP_RCV_SCALE=2 advertises (TCP_WND >> 2) = 23.4 KB on the 51 + * wire and multiplies by 4 on receipt, giving up to 256 KB of 52 + * effective window headroom without further config changes. lwIP 53 + * rejects TCP_WND > u16_t at compile time unless scaling is on. 54 + * 55 + * TCP_SND_BUF stays at 8*MSS because we only send GET requests 56 + * (a few hundred bytes each); larger would just waste heap. */ 57 + #define LWIP_TCP 1 58 + #define TCP_MSS 1460 59 + #define TCP_SND_BUF (8 * TCP_MSS) 60 + #define TCP_WND (64 * TCP_MSS) 61 + #define TCP_SND_QUEUELEN (4 * TCP_SND_BUF / TCP_MSS) 62 + #define LWIP_WND_SCALE 1 63 + #define TCP_RCV_SCALE 2 64 + 65 + /* DNS */ 66 + #define LWIP_DNS 1 67 + #define DNS_MAX_SERVERS 2 68 + 69 + /* IPv4 */ 70 + #define LWIP_IPV4 1 71 + #define LWIP_ARP 1 72 + #define LWIP_ICMP 1 73 + 74 + /* Disable IPv6 */ 75 + #define LWIP_IPV6 0 76 + 77 + /* DHCP */ 78 + #define LWIP_DHCP 1 79 + #define LWIP_DHCP_DOES_ACD_CHECK 0 /* No address conflicts in VM */ 80 + 81 + /* Disable unused features */ 82 + #define LWIP_UDP 1 /* Needed for DNS */ 83 + #define LWIP_RAW 0 84 + #define LWIP_IGMP 0 85 + #define LWIP_AUTOIP 0 86 + #define LWIP_SNMP 0 87 + #define LWIP_PPP 0 88 + 89 + /* Checksum offloading — do it in software */ 90 + #define CHECKSUM_GEN_IP 1 91 + #define CHECKSUM_GEN_UDP 1 92 + #define CHECKSUM_GEN_TCP 1 93 + #define CHECKSUM_CHECK_IP 1 94 + #define CHECKSUM_CHECK_UDP 1 95 + #define CHECKSUM_CHECK_TCP 1 96 + 97 + /* Disable stats to avoid pulling in stats module */ 98 + #define LWIP_STATS 0 99 + 100 + /* Debug (disable for production) */ 101 + #define LWIP_DEBUG 0 102 + 103 + #endif /* LWIPOPTS_H */
+363
net/minilib.c
··· 1 + /* 2 + * minilib.c — Minimal libc stubs for Net.prg (freestanding) 3 + * 4 + * Provides the small set of libc functions needed by our code and by 5 + * the vendored libraries (BearSSL, jsmn, lwIP, picohttpparser). 6 + * Linked into the ELF binary. 7 + */ 8 + #include <stddef.h> 9 + #include <stdint.h> 10 + 11 + /* Forward declarations for internal use */ 12 + void *memset(void *s, int c, size_t n); 13 + void *memcpy(void *dest, const void *src, size_t n); 14 + void free(void *ptr); 15 + 16 + /* ---- TempleOS callbacks (set during init, NULL = use fallbacks) ---- */ 17 + 18 + static uint64_t (*cb_rand)(void); 19 + static uint64_t (*cb_ticks)(void); 20 + static void *(*cb_malloc)(uint64_t); 21 + static void (*cb_free)(void *); 22 + 23 + void minilib_set_callbacks( 24 + uint64_t (*rand_fn)(void), 25 + uint64_t (*ticks_fn)(void), 26 + void *(*malloc_fn)(uint64_t), 27 + void (*free_fn)(void *) 28 + ) { 29 + cb_rand = rand_fn; 30 + cb_ticks = ticks_fn; 31 + cb_malloc = malloc_fn; 32 + cb_free = free_fn; 33 + } 34 + 35 + /* ---- Memory ---- */ 36 + 37 + extern char _end; 38 + static char *heap_ptr = 0; 39 + static char *heap_end = 0; 40 + 41 + void minilib_init_heap(void *start, void *end) { 42 + heap_ptr = (char *)start; 43 + heap_end = (char *)end; 44 + } 45 + 46 + void *malloc(size_t size) { 47 + if (cb_malloc) return cb_malloc((uint64_t)size); 48 + /* Fallback: bump allocator */ 49 + if (!heap_ptr) return NULL; 50 + size = (size + 15) & ~(size_t)15; 51 + if (heap_ptr + size > heap_end) return NULL; 52 + void *p = heap_ptr; 53 + heap_ptr += size; 54 + return p; 55 + } 56 + 57 + void *calloc(size_t n, size_t size) { 58 + size_t total = n * size; 59 + void *p = malloc(total); 60 + if (p) memset(p, 0, total); 61 + return p; 62 + } 63 + 64 + void *realloc(void *ptr, size_t size) { 65 + if (!ptr) return malloc(size); 66 + /* WARNING: Without old-size tracking, copies `size` bytes from old buffer. 67 + * If new_size > old_size, reads past old allocation (harmless in TempleOS 68 + * flat address space but copies garbage). Callers with known valid-data 69 + * lengths should use malloc+memcpy(valid_len)+free instead. */ 70 + void *newp = malloc(size); 71 + if (newp) { 72 + memcpy(newp, ptr, size); 73 + free(ptr); 74 + } 75 + return newp; 76 + } 77 + 78 + void free(void *ptr) { 79 + if (cb_free && ptr) { cb_free(ptr); return; } 80 + /* Bump allocator fallback: no-op */ 81 + } 82 + 83 + /* ---- String/memory functions ---- */ 84 + 85 + void *memcpy(void *dest, const void *src, size_t n) { 86 + unsigned char *d = dest; 87 + const unsigned char *s = src; 88 + while (n--) *d++ = *s++; 89 + return dest; 90 + } 91 + 92 + void *memmove(void *dest, const void *src, size_t n) { 93 + unsigned char *d = dest; 94 + const unsigned char *s = src; 95 + if (d < s) { 96 + while (n--) *d++ = *s++; 97 + } else { 98 + d += n; s += n; 99 + while (n--) *--d = *--s; 100 + } 101 + return dest; 102 + } 103 + 104 + void *memset(void *s, int c, size_t n) { 105 + unsigned char *p = s; 106 + while (n--) *p++ = (unsigned char)c; 107 + return s; 108 + } 109 + 110 + int memcmp(const void *s1, const void *s2, size_t n) { 111 + const unsigned char *a = s1, *b = s2; 112 + while (n--) { 113 + if (*a != *b) return *a - *b; 114 + a++; b++; 115 + } 116 + return 0; 117 + } 118 + 119 + size_t strlen(const char *s) { 120 + const char *p = s; 121 + while (*p) p++; 122 + return (size_t)(p - s); 123 + } 124 + 125 + char *strcpy(char *dest, const char *src) { 126 + char *d = dest; 127 + while ((*d++ = *src++)); 128 + return dest; 129 + } 130 + 131 + char *strncpy(char *dest, const char *src, size_t n) { 132 + char *d = dest; 133 + while (n && (*d++ = *src++)) n--; 134 + while (n--) *d++ = 0; 135 + return dest; 136 + } 137 + 138 + int strcmp(const char *s1, const char *s2) { 139 + while (*s1 && *s1 == *s2) { s1++; s2++; } 140 + return *(unsigned char *)s1 - *(unsigned char *)s2; 141 + } 142 + 143 + int strncmp(const char *s1, const char *s2, size_t n) { 144 + while (n && *s1 && *s1 == *s2) { s1++; s2++; n--; } 145 + return n ? *(unsigned char *)s1 - *(unsigned char *)s2 : 0; 146 + } 147 + 148 + char *strchr(const char *s, int c) { 149 + while (*s) { 150 + if (*s == (char)c) return (char *)s; 151 + s++; 152 + } 153 + return (c == 0) ? (char *)s : NULL; 154 + } 155 + 156 + char *strrchr(const char *s, int c) { 157 + const char *last = NULL; 158 + while (*s) { 159 + if (*s == (char)c) last = s; 160 + s++; 161 + } 162 + return (c == 0) ? (char *)s : (char *)last; 163 + } 164 + 165 + int tolower(int c) { 166 + return (c >= 'A' && c <= 'Z') ? c + 32 : c; 167 + } 168 + 169 + /* ---- Formatted output (minimal snprintf) ---- */ 170 + 171 + /* We need snprintf for HTTP request construction. 172 + * Minimal implementation supporting %s, %d, %zu, %x, %X, %%. */ 173 + 174 + static int fmt_int(char *buf, size_t sz, size_t *pos, long long val, int base, int upper) { 175 + char tmp[24]; 176 + int neg = val < 0 && base == 10; 177 + unsigned long long uval = neg ? -val : val; 178 + int i = 0; 179 + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; 180 + if (uval == 0) tmp[i++] = '0'; 181 + while (uval) { tmp[i++] = digits[uval % base]; uval /= base; } 182 + if (neg) tmp[i++] = '-'; 183 + int written = 0; 184 + while (i--) { 185 + if (*pos < sz - 1) buf[(*pos)++] = tmp[i]; 186 + written++; 187 + } 188 + return written; 189 + } 190 + 191 + int vsnprintf(char *buf, size_t sz, const char *fmt, __builtin_va_list ap) { 192 + size_t pos = 0; 193 + int total = 0; 194 + 195 + while (*fmt) { 196 + if (*fmt != '%') { 197 + if (pos < sz - 1) buf[pos++] = *fmt; 198 + fmt++; total++; continue; 199 + } 200 + fmt++; /* skip % */ 201 + int is_long = 0, is_size = 0; 202 + if (*fmt == 'l') { is_long = 1; fmt++; if (*fmt == 'l') { fmt++; } } 203 + if (*fmt == 'z') { is_size = 1; fmt++; } 204 + switch (*fmt) { 205 + case 's': { 206 + const char *s = __builtin_va_arg(ap, const char *); 207 + if (!s) s = "(null)"; 208 + while (*s) { 209 + if (pos < sz - 1) buf[pos++] = *s; 210 + s++; total++; 211 + } 212 + break; 213 + } 214 + case 'd': case 'i': { 215 + long long v = is_size ? (long long)__builtin_va_arg(ap, size_t) 216 + : is_long ? __builtin_va_arg(ap, long long) 217 + : (long long)__builtin_va_arg(ap, int); 218 + total += fmt_int(buf, sz, &pos, v, 10, 0); 219 + break; 220 + } 221 + case 'u': { 222 + unsigned long long v = is_size ? (unsigned long long)__builtin_va_arg(ap, size_t) 223 + : is_long ? __builtin_va_arg(ap, unsigned long long) 224 + : (unsigned long long)__builtin_va_arg(ap, unsigned int); 225 + total += fmt_int(buf, sz, &pos, (long long)v, 10, 0); 226 + break; 227 + } 228 + case 'x': case 'X': { 229 + unsigned long long v = is_size ? (unsigned long long)__builtin_va_arg(ap, size_t) 230 + : is_long ? __builtin_va_arg(ap, unsigned long long) 231 + : (unsigned long long)__builtin_va_arg(ap, unsigned int); 232 + total += fmt_int(buf, sz, &pos, (long long)v, 16, *fmt == 'X'); 233 + break; 234 + } 235 + case 'p': { 236 + void *p = __builtin_va_arg(ap, void *); 237 + total += fmt_int(buf, sz, &pos, (long long)(uintptr_t)p, 16, 0); 238 + break; 239 + } 240 + case 'c': { 241 + char c = (char)__builtin_va_arg(ap, int); 242 + if (pos < sz - 1) buf[pos++] = c; 243 + total++; 244 + break; 245 + } 246 + case '%': 247 + if (pos < sz - 1) buf[pos++] = '%'; 248 + total++; 249 + break; 250 + default: 251 + /* Unknown format — just output the character */ 252 + if (pos < sz - 1) buf[pos++] = *fmt; 253 + total++; 254 + break; 255 + } 256 + fmt++; 257 + } 258 + if (sz > 0) buf[pos < sz ? pos : sz - 1] = '\0'; 259 + return total; 260 + } 261 + 262 + int snprintf(char *buf, size_t sz, const char *fmt, ...) { 263 + __builtin_va_list ap; 264 + __builtin_va_start(ap, fmt); 265 + int r = vsnprintf(buf, sz, fmt, ap); 266 + __builtin_va_end(ap); 267 + return r; 268 + } 269 + 270 + /* No strtod — we use jsmn instead of cJSON, no floating point needed */ 271 + 272 + /* ---- bcmp (used by some compilers as memcmp shortcut) ---- */ 273 + 274 + int bcmp(const void *s1, const void *s2, size_t n) { 275 + return memcmp(s1, s2, n); 276 + } 277 + 278 + /* ---- time (BearSSL x509 uses this for cert validation) ---- */ 279 + 280 + typedef long time_t; 281 + time_t time(time_t *t) { 282 + /* Return a plausible timestamp — April 2026 in Unix epoch */ 283 + time_t now = 1775000000; 284 + if (t) *t = now; 285 + return now; 286 + } 287 + 288 + /* ---- Character classification ---- */ 289 + 290 + int isxdigit(int c) { 291 + return (c >= '0' && c <= '9') || 292 + (c >= 'a' && c <= 'f') || 293 + (c >= 'A' && c <= 'F'); 294 + } 295 + 296 + int isdigit(int c) { return c >= '0' && c <= '9'; } 297 + int isalpha(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } 298 + int isspace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } 299 + 300 + int atoi(const char *s) { 301 + int result = 0, neg = 0; 302 + while (isspace(*s)) s++; 303 + if (*s == '-') { neg = 1; s++; } 304 + else if (*s == '+') s++; 305 + while (*s >= '0' && *s <= '9') 306 + result = result * 10 + (*s++ - '0'); 307 + return neg ? -result : result; 308 + } 309 + 310 + /* ---- errno ---- */ 311 + 312 + static int errno_val; 313 + int *__errno_location(void) { return &errno_val; } 314 + 315 + /* ---- File descriptor stubs (BearSSL sysrng tries /dev/urandom) ---- */ 316 + 317 + int open(const char *path, int flags, ...) { (void)path; (void)flags; return -1; } 318 + int close(int fd) { (void)fd; return 0; } 319 + long read(int fd, void *buf, unsigned long n) { (void)fd; (void)buf; (void)n; return -1; } 320 + 321 + /* ---- Misc stubs ---- */ 322 + 323 + /* BearSSL's sysrng needs a random source. Stub for now. */ 324 + /* The real implementation would call out to TempleOS's RandU64(). */ 325 + int getentropy(void *buf, size_t len) { 326 + unsigned char *p = buf; 327 + if (cb_rand) { 328 + /* Use TempleOS RandU64 for real randomness */ 329 + for (size_t i = 0; i < len; i += 8) { 330 + uint64_t r = cb_rand(); 331 + size_t chunk = len - i; 332 + if (chunk > 8) chunk = 8; 333 + memcpy(p + i, &r, chunk); 334 + } 335 + return 0; 336 + } 337 + /* Fallback: deterministic LCG */ 338 + static unsigned long long seed = 0x12345678DEADBEEF; 339 + for (size_t i = 0; i < len; i++) { 340 + seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; 341 + p[i] = (unsigned char)(seed >> 33); 342 + } 343 + return 0; 344 + } 345 + 346 + unsigned int minilib_lwip_rand(void) { 347 + if (cb_rand) { 348 + return (unsigned int)cb_rand(); 349 + } 350 + static unsigned int seed = 0xDEADBEEF; 351 + seed = seed * 1103515245 + 12345; 352 + return seed; 353 + } 354 + 355 + unsigned int sys_now(void) { 356 + if (cb_ticks) { 357 + /* TempleOS tick counter — returns milliseconds */ 358 + return (unsigned int)cb_ticks(); 359 + } 360 + /* Fallback: synthetic time (10ms per call) */ 361 + static unsigned int ticks = 0; 362 + return ticks += 10; 363 + }
+1141
net/net_api.c
··· 1 + /* 2 + * net_api.c -- VodBrowser network library implementation 3 + * 4 + * Uses lwIP (raw TCP API, NO_SYS=1) + BearSSL + picohttpparser + jsmn. 5 + * All network I/O goes through lwIP, which calls out to the HolyC NIC 6 + * driver via function pointers registered at init time. 7 + */ 8 + #include "net_api.h" 9 + #include "tos_netif.h" 10 + 11 + #include <string.h> 12 + #include <stdlib.h> 13 + #include <stdio.h> 14 + 15 + extern unsigned int sys_now(void); 16 + 17 + #include "bearssl.h" 18 + #include "picohttpparser.h" 19 + #include "json_wrap.h" 20 + 21 + #include "lwip/tcp.h" 22 + #include "lwip/dns.h" 23 + #include "lwip/timeouts.h" 24 + #include "lwip/pbuf.h" 25 + #include "lwip/ip4_addr.h" 26 + #include "lwip/netif.h" 27 + 28 + /* ---- Blocking TCP connection over lwIP raw API ---- */ 29 + 30 + /* 31 + * Since lwIP in NO_SYS mode is event-driven, we need to spin-poll 32 + * in a loop to make blocking calls. Each call to tos_netif_poll() 33 + * processes incoming packets and timers. 34 + */ 35 + 36 + typedef struct { 37 + struct tcp_pcb *pcb; 38 + /* Receive buffer: accumulates incoming data */ 39 + unsigned char *rxbuf; 40 + size_t rxlen; 41 + size_t rxcap; 42 + /* State flags */ 43 + int connected; 44 + int closed; 45 + int error; 46 + } tcp_conn_t; 47 + 48 + static err_t tcp_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) 49 + { 50 + tcp_conn_t *conn = (tcp_conn_t *)arg; 51 + if (!p || err != ERR_OK) { 52 + conn->closed = 1; 53 + if (p) pbuf_free(p); 54 + return ERR_OK; 55 + } 56 + /* Grow receive buffer (manual malloc+copy — our realloc lacks old-size tracking) */ 57 + while (conn->rxlen + p->tot_len > conn->rxcap) { 58 + size_t newcap = conn->rxcap ? conn->rxcap * 2 : 4096; 59 + unsigned char *newbuf = malloc(newcap); 60 + if (!newbuf) { conn->error = 1; if (p) pbuf_free(p); return ERR_MEM; } 61 + if (conn->rxbuf) { 62 + memcpy(newbuf, conn->rxbuf, conn->rxlen); 63 + free(conn->rxbuf); 64 + } 65 + conn->rxbuf = newbuf; 66 + conn->rxcap = newcap; 67 + } 68 + pbuf_copy_partial(p, conn->rxbuf + conn->rxlen, p->tot_len, 0); 69 + conn->rxlen += p->tot_len; 70 + tcp_recved(tpcb, p->tot_len); 71 + pbuf_free(p); 72 + return ERR_OK; 73 + } 74 + 75 + static err_t tcp_connected_cb(void *arg, struct tcp_pcb *tpcb, err_t err) 76 + { 77 + (void)tpcb; 78 + tcp_conn_t *conn = (tcp_conn_t *)arg; 79 + if (err == ERR_OK) 80 + conn->connected = 1; 81 + else 82 + conn->error = 1; 83 + return ERR_OK; 84 + } 85 + 86 + static void tcp_error_cb(void *arg, err_t err) 87 + { 88 + (void)err; 89 + tcp_conn_t *conn = (tcp_conn_t *)arg; 90 + conn->error = 1; 91 + conn->pcb = NULL; /* pcb is freed by lwIP on error */ 92 + } 93 + 94 + /* Spin-poll until a condition is met. 95 + * Throttled to ~100 polls/sec — tight polling breaks TCP on QEMU 96 + * (SYNs never make it out, likely a DMA/timer interaction). */ 97 + static void poll_until(int *flag, int timeout_ms) 98 + { 99 + uint32_t start = sys_now(); 100 + while (!*flag && (sys_now() - start) < (uint32_t)timeout_ms) { 101 + tos_netif_poll(); 102 + /* Busy-wait ~10ms between polls. Can't Sleep (big stack), 103 + * but need spacing for QEMU PCnet + lwIP timers to work. */ 104 + uint32_t next = sys_now() + 10; 105 + while (sys_now() < next) 106 + ; 107 + } 108 + } 109 + 110 + /* Non-blocking poll: run one iteration and return. 111 + * HolyC calls this in a loop with Sleep() between calls. */ 112 + void net_poll_once(void) { 113 + tos_netif_poll(); 114 + } 115 + 116 + /* Blocking TCP write */ 117 + static int tcp_write_all(tcp_conn_t *conn, const void *data, size_t len) 118 + { 119 + const unsigned char *p = (const unsigned char *)data; 120 + while (len > 0 && !conn->error) { 121 + size_t chunk = len; 122 + err_t err = tcp_write(conn->pcb, p, (u16_t)(chunk > 0xFFFF ? 0xFFFF : chunk), 123 + TCP_WRITE_FLAG_COPY); 124 + if (err == ERR_MEM) { 125 + tcp_output(conn->pcb); 126 + tos_netif_poll(); 127 + continue; 128 + } 129 + if (err != ERR_OK) return -1; 130 + tcp_output(conn->pcb); 131 + p += chunk; 132 + len -= chunk; 133 + } 134 + return conn->error ? -1 : 0; 135 + } 136 + 137 + /* ---- BearSSL I/O callbacks over lwIP TCP ---- */ 138 + 139 + static int br_tcp_read(void *ctx, unsigned char *buf, size_t len) 140 + { 141 + tcp_conn_t *conn = (tcp_conn_t *)ctx; 142 + /* Wait for data */ 143 + while (conn->rxlen == 0 && !conn->closed && !conn->error) 144 + tos_netif_poll(); 145 + if (conn->rxlen == 0) return -1; 146 + size_t n = len < conn->rxlen ? len : conn->rxlen; 147 + memcpy(buf, conn->rxbuf, n); 148 + memmove(conn->rxbuf, conn->rxbuf + n, conn->rxlen - n); 149 + conn->rxlen -= n; 150 + return (int)n; 151 + } 152 + 153 + static int br_tcp_write(void *ctx, const unsigned char *buf, size_t len) 154 + { 155 + tcp_conn_t *conn = (tcp_conn_t *)ctx; 156 + if (tcp_write_all(conn, buf, len) < 0) return -1; 157 + return (int)len; 158 + } 159 + 160 + /* ---- No-anchor X.509 wrapper (PoC — skips cert verification) ---- */ 161 + 162 + typedef struct { 163 + const br_x509_class *vtable; 164 + const br_x509_class **inner; 165 + } x509_noanchor_ctx; 166 + 167 + static void na_sc(const br_x509_class **c, const char *s) { 168 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; (*w->inner)->start_chain(w->inner, s); } 169 + static void na_scr(const br_x509_class **c, uint32_t l) { 170 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; (*w->inner)->start_cert(w->inner, l); } 171 + static void na_a(const br_x509_class **c, const unsigned char *b, size_t l) { 172 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; (*w->inner)->append(w->inner, b, l); } 173 + static void na_ec(const br_x509_class **c) { 174 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; (*w->inner)->end_cert(w->inner); } 175 + static unsigned na_ech(const br_x509_class **c) { 176 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; 177 + unsigned r = (*w->inner)->end_chain(w->inner); 178 + return r == BR_ERR_X509_NOT_TRUSTED ? 0 : r; } 179 + static const br_x509_pkey *na_gp(const br_x509_class *const *c, unsigned *u) { 180 + x509_noanchor_ctx *w = (x509_noanchor_ctx *)c; return (*w->inner)->get_pkey(w->inner, u); } 181 + 182 + static const br_x509_class x509_noanchor_vtable = { 183 + sizeof(x509_noanchor_ctx), na_sc, na_scr, na_a, na_ec, na_ech, na_gp 184 + }; 185 + 186 + /* ---- URL parser ---- */ 187 + 188 + static int parse_url(const char *url, char *host, size_t hsz, 189 + char *path, size_t psz, uint16_t *port) 190 + { 191 + *port = 443; 192 + if (strncmp(url, "https://", 8) != 0) return -1; 193 + const char *h = url + 8; 194 + const char *slash = strchr(h, '/'); 195 + const char *colon = strchr(h, ':'); 196 + if (colon && (!slash || colon < slash)) { 197 + size_t hlen = (size_t)(colon - h); 198 + if (hlen >= hsz) return -1; 199 + memcpy(host, h, hlen); host[hlen] = 0; 200 + *port = 0; 201 + for (const char *p = colon + 1; *p && *p != '/'; p++) 202 + *port = *port * 10 + (*p - '0'); 203 + h = slash ? slash : colon + 1; 204 + } else if (slash) { 205 + size_t hlen = (size_t)(slash - h); 206 + if (hlen >= hsz) return -1; 207 + memcpy(host, h, hlen); host[hlen] = 0; 208 + } else { 209 + size_t hlen = strlen(h); 210 + if (hlen >= hsz) return -1; 211 + memcpy(host, h, hlen); host[hlen] = 0; 212 + } 213 + if (slash) { size_t plen = strlen(slash); if (plen >= psz) return -1; memcpy(path, slash, plen + 1); } 214 + else { path[0] = '/'; path[1] = 0; } 215 + return 0; 216 + } 217 + 218 + /* ---- DNS resolution (blocking) ---- */ 219 + 220 + static ip_addr_t resolved_addr; 221 + static int dns_done; 222 + 223 + static void dns_cb(const char *name, const ip_addr_t *addr, void *arg) 224 + { 225 + (void)name; (void)arg; 226 + if (addr) resolved_addr = *addr; 227 + dns_done = 1; 228 + } 229 + 230 + static int resolve_host(const char *host, ip_addr_t *addr) 231 + { 232 + dns_done = 0; 233 + err_t err = dns_gethostbyname(host, &resolved_addr, dns_cb, NULL); 234 + if (err == ERR_OK) { 235 + *addr = resolved_addr; 236 + return 0; 237 + } 238 + if (err == ERR_INPROGRESS) { 239 + poll_until(&dns_done, 10000); 240 + if (dns_done) { *addr = resolved_addr; return 0; } 241 + } 242 + return -1; 243 + } 244 + 245 + /* ---- HTTPS fetch implementation ---- */ 246 + 247 + static int https_get(const char *host, uint16_t port, const char *path, 248 + const char *extra_headers, net_response_t *resp) 249 + { 250 + memset(resp, 0, sizeof(*resp)); 251 + 252 + /* DNS resolve */ 253 + ip_addr_t addr; 254 + if (resolve_host(host, &addr) < 0) return -10; 255 + 256 + /* TCP connect */ 257 + tcp_conn_t conn; 258 + memset(&conn, 0, sizeof(conn)); 259 + conn.pcb = tcp_new(); 260 + if (!conn.pcb) return -20; 261 + 262 + tcp_arg(conn.pcb, &conn); 263 + tcp_recv(conn.pcb, tcp_recv_cb); 264 + tcp_err(conn.pcb, tcp_error_cb); 265 + 266 + tcp_connect(conn.pcb, &addr, port, tcp_connected_cb); 267 + poll_until(&conn.connected, 10000); 268 + if (!conn.connected || conn.error) { 269 + if (conn.pcb) tcp_abort(conn.pcb); 270 + return -30; 271 + } 272 + 273 + /* TLS handshake */ 274 + /* Heap-allocate BearSSL contexts — too large for TempleOS stack */ 275 + br_ssl_client_context *sc = malloc(sizeof(br_ssl_client_context)); 276 + br_x509_minimal_context *xc = malloc(sizeof(br_x509_minimal_context)); 277 + unsigned char *iobuf = malloc(BR_SSL_BUFSIZE_BIDI); 278 + br_sslio_context *ioc = malloc(sizeof(br_sslio_context)); 279 + x509_noanchor_ctx na; 280 + 281 + if (!sc || !xc || !iobuf || !ioc) { 282 + free(sc); free(xc); free(iobuf); free(ioc); 283 + tcp_abort(conn.pcb); return -40; 284 + } 285 + 286 + br_ssl_client_init_full(sc, xc, NULL, 0); 287 + na.vtable = &x509_noanchor_vtable; 288 + na.inner = &xc->vtable; 289 + br_ssl_engine_set_x509(&sc->eng, &na.vtable); 290 + br_ssl_engine_set_buffer(&sc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1); 291 + br_ssl_client_reset(sc, host, 0); 292 + br_sslio_init(ioc, &sc->eng, br_tcp_read, &conn, br_tcp_write, &conn); 293 + 294 + /* Send HTTP request */ 295 + char req[4096]; 296 + int reqlen = 0; 297 + reqlen += snprintf(req + reqlen, sizeof(req) - reqlen, 298 + "GET %s HTTP/1.0\r\nHost: %s\r\nAccept: */*\r\nConnection: close\r\n", 299 + path, host); 300 + if (extra_headers) 301 + reqlen += snprintf(req + reqlen, sizeof(req) - reqlen, "%s", extra_headers); 302 + reqlen += snprintf(req + reqlen, sizeof(req) - reqlen, "\r\n"); 303 + 304 + br_sslio_write_all(ioc, req, reqlen); 305 + br_sslio_flush(ioc); 306 + 307 + /* Read response */ 308 + size_t cap = 1 << 16, len = 0; 309 + unsigned char *buf = malloc(cap); 310 + if (!buf) { tcp_abort(conn.pcb); return -50; } 311 + 312 + for (;;) { 313 + if (len + 16384 > cap) { 314 + size_t newcap = cap * 2; 315 + unsigned char *tmp = malloc(newcap); 316 + if (!tmp) break; 317 + memcpy(tmp, buf, len); 318 + free(buf); 319 + buf = tmp; 320 + cap = newcap; 321 + } 322 + int rlen = br_sslio_read(ioc, buf + len, 16384); 323 + if (rlen < 0) break; 324 + len += (size_t)rlen; 325 + } 326 + 327 + /* Close TCP */ 328 + if (conn.pcb) { 329 + tcp_arg(conn.pcb, NULL); 330 + tcp_recv(conn.pcb, NULL); 331 + tcp_err(conn.pcb, NULL); 332 + tcp_close(conn.pcb); 333 + } 334 + 335 + if (len == 0) { 336 + /* Capture error BEFORE freeing sc */ 337 + int err = br_ssl_engine_last_error(&sc->eng); 338 + free(buf); 339 + free(conn.rxbuf); 340 + free(sc); free(xc); free(iobuf); free(ioc); 341 + return -600 - err; 342 + } 343 + 344 + free(conn.rxbuf); 345 + free(sc); free(xc); free(iobuf); free(ioc); 346 + 347 + /* Parse HTTP response */ 348 + int minor_version, status; 349 + const char *msg; 350 + size_t msg_len; 351 + struct phr_header headers[64]; 352 + size_t num_headers = 64; 353 + 354 + int hdr_len = phr_parse_response((const char *)buf, len, 355 + &minor_version, &status, &msg, &msg_len, 356 + headers, &num_headers, 0); 357 + 358 + if (hdr_len < 0) { free(buf); return -70; } 359 + 360 + resp->status = status; 361 + resp->body_len = len - (size_t)hdr_len; 362 + resp->body = malloc(resp->body_len); 363 + if (!resp->body) { free(buf); return -80; } 364 + memcpy(resp->body, buf + hdr_len, resp->body_len); 365 + free(buf); 366 + return 0; 367 + } 368 + 369 + /* ---- Public API ---- */ 370 + 371 + int net_fetch(const char *url, net_response_t *resp) 372 + { 373 + char host[256], path[2048]; 374 + uint16_t port; 375 + if (parse_url(url, host, sizeof host, path, sizeof path, &port) != 0) return -1; 376 + /* Retry up to 3 times — QEMU NAT can be flaky on first connect */ 377 + int rc; 378 + for (int i = 0; i < 3; i++) { 379 + rc = https_get(host, port, path, NULL, resp); 380 + if (rc == 0) return 0; 381 + } 382 + return rc; 383 + } 384 + 385 + int net_fetch_range(const char *url, size_t range_start, size_t range_end, 386 + net_response_t *resp) 387 + { 388 + char host[256], path[2048], hdr[128]; 389 + uint16_t port; 390 + if (parse_url(url, host, sizeof host, path, sizeof path, &port) != 0) return -1; 391 + if (range_end > 0) 392 + snprintf(hdr, sizeof hdr, "Range: bytes=%zu-%zu\r\n", range_start, range_end); 393 + else 394 + snprintf(hdr, sizeof hdr, "Range: bytes=%zu-\r\n", range_start); 395 + return https_get(host, port, path, hdr, resp); 396 + } 397 + 398 + void net_free_response(net_response_t *resp) { 399 + free(resp->body); 400 + memset(resp, 0, sizeof(*resp)); 401 + } 402 + 403 + static void net_poll(void) { tos_netif_poll(); } 404 + void net_poll_internal(void) { tos_netif_poll(); } 405 + 406 + /* ==== Async Fetch State Machine ==== */ 407 + 408 + /* Extra headers for the async fetch (e.g. Range). HolyC sets this via 409 + * net_set_fetch_headers() right before net_fetch_begin(); it is cleared 410 + * inside the state machine after the HTTP request is built. */ 411 + static char async_extra_headers[256]; 412 + 413 + void net_set_fetch_headers(const char *hdrs) 414 + { 415 + if (hdrs) { 416 + size_t len = strlen(hdrs); 417 + if (len >= sizeof(async_extra_headers)) len = sizeof(async_extra_headers) - 1; 418 + memcpy(async_extra_headers, hdrs, len); 419 + async_extra_headers[len] = 0; 420 + } else { 421 + async_extra_headers[0] = 0; 422 + } 423 + } 424 + 425 + enum fetch_phase { 426 + FP_IDLE = 0, 427 + FP_DNS, 428 + FP_TCP_CONNECT, 429 + FP_TLS_HANDSHAKE, 430 + FP_HTTP_SEND, 431 + FP_HTTP_RECV, 432 + FP_DONE, 433 + FP_ERROR, 434 + }; 435 + 436 + #define FETCH_PHASE_TIMEOUT 15000 437 + 438 + static struct { 439 + enum fetch_phase phase; 440 + 441 + /* ---- Per-fetch transient state (cleared by fetch_reset_for_next) ---- */ 442 + char path[2048]; 443 + char req[4096]; 444 + size_t req_len; 445 + size_t req_sent; 446 + unsigned char *resp_buf; 447 + size_t resp_len; 448 + size_t resp_cap; 449 + size_t body_start; /* offset of body in resp_buf (set once headers_done) */ 450 + size_t content_length; /* parsed from Content-Length, 0 = unknown */ 451 + int headers_done; /* 1 once \r\n\r\n has been seen and parsed */ 452 + int keep_alive; /* 1 if this response says we can reuse the conn */ 453 + int http_status; /* parsed HTTP status code */ 454 + uint32_t phase_start; 455 + int error_code; 456 + /* Per-fetch flag: set to 1 after we've already retried this fetch 457 + * once on a fresh connection because the reused keep-alive conn 458 + * died on us. Prevents infinite retry loops if the second attempt 459 + * also fails. Cleared at the top of net_fetch_begin. */ 460 + int retried_reuse; 461 + 462 + /* ---- Persistent connection state (survives across fetches) ---- */ 463 + char host[256]; 464 + uint16_t port; 465 + int conn_valid; /* 1 when (host, port, conn, sc) are reusable */ 466 + tcp_conn_t conn; 467 + br_ssl_client_context *sc; 468 + br_x509_minimal_context *xc; 469 + unsigned char *iobuf; 470 + x509_noanchor_ctx na; 471 + } fctx; 472 + 473 + /* Diagnostic counter set by net_fetch_begin based on whether the 474 + * just-started fetch is going to reuse the existing TLS connection or 475 + * reopen fresh. NetTask reads this via net_get_last_reused() and 476 + * surfaces it in the debug log so we can see whether HTTP keep-alive 477 + * is actually firing. */ 478 + static int g_last_fetch_reused; 479 + 480 + int net_get_last_reused(void) { return g_last_fetch_reused; } 481 + 482 + static void fetch_set_phase(enum fetch_phase p) 483 + { 484 + fctx.phase = p; 485 + fctx.phase_start = sys_now(); 486 + } 487 + 488 + static int fetch_timed_out(void) 489 + { 490 + return (sys_now() - fctx.phase_start) > FETCH_PHASE_TIMEOUT; 491 + } 492 + 493 + /* Fully tear down the TCP + TLS connection and free all associated 494 + * state. Used on errors and when a server signals Connection: close. */ 495 + static void fetch_close_conn_full(void) 496 + { 497 + if (fctx.conn.pcb) { 498 + tcp_arg(fctx.conn.pcb, NULL); 499 + tcp_recv(fctx.conn.pcb, NULL); 500 + tcp_err(fctx.conn.pcb, NULL); 501 + if (tcp_close(fctx.conn.pcb) != ERR_OK) 502 + tcp_abort(fctx.conn.pcb); 503 + fctx.conn.pcb = NULL; 504 + } 505 + free(fctx.conn.rxbuf); fctx.conn.rxbuf = NULL; 506 + fctx.conn.rxlen = 0; 507 + fctx.conn.rxcap = 0; 508 + fctx.conn.connected = 0; 509 + fctx.conn.closed = 0; 510 + fctx.conn.error = 0; 511 + free(fctx.sc); fctx.sc = NULL; 512 + free(fctx.xc); fctx.xc = NULL; 513 + free(fctx.iobuf); fctx.iobuf = NULL; 514 + fctx.conn_valid = 0; 515 + } 516 + 517 + /* Clear per-fetch transient state but LEAVE the connection alive 518 + * (sc/xc/iobuf/conn untouched, host/port/conn_valid preserved). Used 519 + * between fetches that reuse the same keep-alive connection. */ 520 + static void fetch_reset_for_next(void) 521 + { 522 + free(fctx.resp_buf); fctx.resp_buf = NULL; 523 + fctx.resp_len = 0; 524 + fctx.resp_cap = 0; 525 + fctx.body_start = 0; 526 + fctx.content_length = 0; 527 + fctx.headers_done = 0; 528 + fctx.keep_alive = 0; 529 + fctx.req_len = 0; 530 + fctx.req_sent = 0; 531 + fctx.error_code = 0; 532 + memset(fctx.path, 0, sizeof(fctx.path)); 533 + memset(fctx.req, 0, sizeof(fctx.req)); 534 + } 535 + 536 + /* Build the HTTP/1.1 GET request into fctx.req. We explicitly ask for 537 + * keep-alive; the server may honour or decline it via its own 538 + * Connection: header in the response. */ 539 + static void fetch_build_request(void) 540 + { 541 + fctx.req_len = (size_t)snprintf(fctx.req, sizeof(fctx.req), 542 + "GET %s HTTP/1.1\r\n" 543 + "Host: %s\r\n" 544 + "Accept: */*\r\n" 545 + "Connection: keep-alive\r\n" 546 + "%s" 547 + "\r\n", 548 + fctx.path, fctx.host, async_extra_headers); 549 + async_extra_headers[0] = 0; /* clear after use */ 550 + fctx.req_sent = 0; 551 + } 552 + 553 + static void fetch_fail(int code) 554 + { 555 + fctx.error_code = code; 556 + fetch_close_conn_full(); 557 + free(fctx.resp_buf); fctx.resp_buf = NULL; 558 + fctx.resp_len = 0; 559 + fctx.resp_cap = 0; 560 + fctx.phase = FP_ERROR; 561 + } 562 + 563 + /* One-shot recovery from a dead HTTP keep-alive connection. Called 564 + * from the FP_HTTP_RECV close-without-data branches when the server 565 + * closed the reused connection before we got any response bytes. In 566 + * practice this happens after N requests on a single keep-alive 567 + * conn — the CDN enforces a per-connection request limit and closes 568 + * mid-stream from our perspective. 569 + * 570 + * Returns 1 if a retry was started (caller returns NET_FETCH_PENDING 571 + * so the state machine keeps ticking on the fresh conn), 0 if no 572 + * retry is allowed (caller should fetch_fail normally). 573 + * 574 + * Notes: 575 + * - Only retries once per fetch (fctx.retried_reuse flag), so a 576 + * genuinely broken server can't spin us forever. 577 + * - Only retries when the failed attempt actually reused a connection 578 + * — a fresh connection that dies immediately is a different kind 579 + * of failure (DNS/routing/TLS) that won't be fixed by a retry. 580 + * - fctx.host/port/path are preserved across the retry; only the 581 + * per-attempt request and response buffers are reset. When the 582 + * state machine reaches FP_TLS_HANDSHAKE again it calls 583 + * fetch_build_request() which reads fctx.path, so we must NOT 584 + * clear it. */ 585 + static int retry_dead_keepalive(void) 586 + { 587 + if (!g_last_fetch_reused || fctx.retried_reuse) return 0; 588 + 589 + fctx.retried_reuse = 1; 590 + fetch_close_conn_full(); 591 + g_last_fetch_reused = 0; 592 + 593 + /* Reset per-attempt state without touching fctx.host/port/path. */ 594 + free(fctx.resp_buf); fctx.resp_buf = NULL; 595 + fctx.resp_len = 0; 596 + fctx.resp_cap = 0; 597 + fctx.body_start = 0; 598 + fctx.content_length = 0; 599 + fctx.headers_done = 0; 600 + fctx.keep_alive = 0; 601 + fctx.req_len = 0; 602 + fctx.req_sent = 0; 603 + fctx.error_code = 0; 604 + memset(fctx.req, 0, sizeof(fctx.req)); 605 + 606 + /* Kick off a fresh DNS → TCP → TLS → HTTP sequence, mirroring the 607 + * "full new connection" path in net_fetch_begin. */ 608 + dns_done = 0; 609 + err_t err = dns_gethostbyname(fctx.host, &resolved_addr, dns_cb, NULL); 610 + if (err == ERR_OK) { 611 + dns_done = 1; 612 + fetch_set_phase(FP_TCP_CONNECT); 613 + } else if (err == ERR_INPROGRESS) { 614 + fetch_set_phase(FP_DNS); 615 + } else { 616 + /* DNS failed synchronously — give up and let caller fail. */ 617 + return 0; 618 + } 619 + return 1; 620 + } 621 + 622 + /* Case-insensitive ASCII compare of a phr header name against a 623 + * constant. Avoids needing strncasecmp in our minilib. */ 624 + static int header_name_eq(const char *h, size_t hlen, const char *name) 625 + { 626 + size_t nlen = strlen(name); 627 + if (hlen != nlen) return 0; 628 + for (size_t i = 0; i < nlen; i++) { 629 + int a = (unsigned char)h[i]; 630 + int b = (unsigned char)name[i]; 631 + if (a >= 'A' && a <= 'Z') a += 32; 632 + if (b >= 'A' && b <= 'Z') b += 32; 633 + if (a != b) return 0; 634 + } 635 + return 1; 636 + } 637 + 638 + /* 639 + * Pump BearSSL record layer: send outgoing TLS records to TCP, 640 + * feed incoming TCP data to TLS engine. 641 + * Returns: 0=ok, 1=closed-normally, -1=tls-error 642 + */ 643 + static int tls_pump_io(void) 644 + { 645 + unsigned st = br_ssl_engine_current_state(&fctx.sc->eng); 646 + 647 + if (st == BR_SSL_CLOSED) { 648 + fctx.error_code = br_ssl_engine_last_error(&fctx.sc->eng); 649 + return (fctx.error_code == 0) ? 1 : -1; 650 + } 651 + 652 + if (st & BR_SSL_SENDREC) { 653 + size_t len; 654 + unsigned char *buf = br_ssl_engine_sendrec_buf(&fctx.sc->eng, &len); 655 + if (len > 0 && fctx.conn.pcb && !fctx.conn.error) { 656 + size_t chunk = len > 0xFFFF ? 0xFFFF : len; 657 + err_t e = tcp_write(fctx.conn.pcb, buf, (u16_t)chunk, 658 + TCP_WRITE_FLAG_COPY); 659 + if (e == ERR_OK) { 660 + br_ssl_engine_sendrec_ack(&fctx.sc->eng, chunk); 661 + tcp_output(fctx.conn.pcb); 662 + } 663 + /* ERR_MEM → retry next tick */ 664 + } 665 + } 666 + 667 + if ((st & BR_SSL_RECVREC) && fctx.conn.rxlen > 0) { 668 + size_t len; 669 + unsigned char *buf = br_ssl_engine_recvrec_buf(&fctx.sc->eng, &len); 670 + if (len > 0) { 671 + size_t feed = fctx.conn.rxlen < len ? fctx.conn.rxlen : len; 672 + memcpy(buf, fctx.conn.rxbuf, feed); 673 + br_ssl_engine_recvrec_ack(&fctx.sc->eng, feed); 674 + memmove(fctx.conn.rxbuf, fctx.conn.rxbuf + feed, 675 + fctx.conn.rxlen - feed); 676 + fctx.conn.rxlen -= feed; 677 + } 678 + } 679 + 680 + return 0; 681 + } 682 + 683 + int net_fetch_begin(const char *url) 684 + { 685 + if (fctx.phase != FP_IDLE) return -1; 686 + 687 + /* Parse the URL into temporaries first so we can compare against 688 + * any currently-open connection before we clobber fctx.host/path. */ 689 + char new_host[256]; 690 + char new_path[2048]; 691 + uint16_t new_port; 692 + if (parse_url(url, new_host, sizeof(new_host), 693 + new_path, sizeof(new_path), &new_port) != 0) 694 + return -1; 695 + 696 + /* If we've got a live keep-alive connection to the same host:port, 697 + * skip DNS + TCP + TLS handshake entirely and jump straight to 698 + * building and sending the new HTTP request. This is the big win: 699 + * every segment after the first pays 0 overhead instead of a full 700 + * TLS handshake. 701 + * 702 + * The BR_SSL_CLOSED engine-state check is subtle but important: 703 + * when we early-exit on Content-Length, the TLS engine has not yet 704 + * processed any trailing close_notify the server may have sent. 705 + * On the NEXT fetch's first tls_pump_io we'd feed the close_notify 706 + * to BearSSL, which would transition to BR_SSL_CLOSED and our send 707 + * would silently fail and time out. Checking the engine state here 708 + * catches that case and forces a clean reconnect. */ 709 + int engine_ok = 0; 710 + if (fctx.sc) { 711 + unsigned est = br_ssl_engine_current_state(&fctx.sc->eng); 712 + engine_ok = !(est & BR_SSL_CLOSED); 713 + } 714 + int can_reuse = fctx.conn_valid 715 + && fctx.conn.pcb 716 + && !fctx.conn.error 717 + && !fctx.conn.closed 718 + && engine_ok 719 + && fctx.port == new_port 720 + && strcmp(fctx.host, new_host) == 0; 721 + 722 + /* Fresh fetch — reset the one-shot retry flag. Both the reuse and 723 + * fresh-connect paths below need this, so we do it once up front. */ 724 + fctx.retried_reuse = 0; 725 + 726 + if (can_reuse) { 727 + g_last_fetch_reused = 1; 728 + fetch_reset_for_next(); 729 + strncpy(fctx.path, new_path, sizeof(fctx.path) - 1); 730 + fctx.path[sizeof(fctx.path) - 1] = 0; 731 + fetch_build_request(); 732 + /* Allocate the response buffer eagerly so FP_HTTP_RECV doesn't 733 + * have to deal with a NULL check on first append. */ 734 + fctx.resp_cap = 1 << 16; 735 + fctx.resp_buf = malloc(fctx.resp_cap); 736 + fctx.resp_len = 0; 737 + if (!fctx.resp_buf) { fetch_close_conn_full(); return -50; } 738 + fetch_set_phase(FP_HTTP_SEND); 739 + return 0; 740 + } 741 + 742 + /* Full new connection: drop any stale state and start at DNS. */ 743 + g_last_fetch_reused = 0; 744 + fetch_close_conn_full(); 745 + /* Clear the transient fields too. We keep fctx.sc/xc/iobuf NULL 746 + * thanks to the close above. */ 747 + fetch_reset_for_next(); 748 + strncpy(fctx.host, new_host, sizeof(fctx.host) - 1); 749 + fctx.host[sizeof(fctx.host) - 1] = 0; 750 + strncpy(fctx.path, new_path, sizeof(fctx.path) - 1); 751 + fctx.path[sizeof(fctx.path) - 1] = 0; 752 + fctx.port = new_port; 753 + 754 + dns_done = 0; 755 + err_t err = dns_gethostbyname(fctx.host, &resolved_addr, dns_cb, NULL); 756 + if (err == ERR_OK) { 757 + dns_done = 1; 758 + fetch_set_phase(FP_TCP_CONNECT); 759 + } else if (err == ERR_INPROGRESS) { 760 + fetch_set_phase(FP_DNS); 761 + } else { 762 + return -10; 763 + } 764 + return 0; 765 + } 766 + 767 + int net_fetch_tick(void) 768 + { 769 + if (fctx.phase == FP_IDLE) return NET_FETCH_IDLE; 770 + if (fctx.phase == FP_DONE) return NET_FETCH_DONE; 771 + if (fctx.phase == FP_ERROR) return NET_FETCH_ERROR; 772 + 773 + tos_netif_poll(); 774 + 775 + switch (fctx.phase) { 776 + 777 + case FP_DNS: 778 + if (dns_done) { 779 + fetch_set_phase(FP_TCP_CONNECT); 780 + } else if (fetch_timed_out()) { 781 + fetch_fail(-10); 782 + return NET_FETCH_ERROR; 783 + } 784 + break; 785 + 786 + case FP_TCP_CONNECT: 787 + if (!fctx.conn.pcb) { 788 + fctx.conn.pcb = tcp_new(); 789 + if (!fctx.conn.pcb) { fetch_fail(-20); return NET_FETCH_ERROR; } 790 + tcp_arg(fctx.conn.pcb, &fctx.conn); 791 + tcp_recv(fctx.conn.pcb, tcp_recv_cb); 792 + tcp_err(fctx.conn.pcb, tcp_error_cb); 793 + tcp_connect(fctx.conn.pcb, &resolved_addr, fctx.port, tcp_connected_cb); 794 + } 795 + if (fctx.conn.connected) { 796 + /* Allocate and init BearSSL */ 797 + fctx.sc = malloc(sizeof(br_ssl_client_context)); 798 + fctx.xc = malloc(sizeof(br_x509_minimal_context)); 799 + fctx.iobuf = malloc(BR_SSL_BUFSIZE_BIDI); 800 + if (!fctx.sc || !fctx.xc || !fctx.iobuf) { 801 + fetch_fail(-40); 802 + return NET_FETCH_ERROR; 803 + } 804 + br_ssl_client_init_full(fctx.sc, fctx.xc, NULL, 0); 805 + fctx.na.vtable = &x509_noanchor_vtable; 806 + fctx.na.inner = &fctx.xc->vtable; 807 + br_ssl_engine_set_x509(&fctx.sc->eng, &fctx.na.vtable); 808 + br_ssl_engine_set_buffer(&fctx.sc->eng, fctx.iobuf, 809 + BR_SSL_BUFSIZE_BIDI, 1); 810 + br_ssl_client_reset(fctx.sc, fctx.host, 0); 811 + fetch_set_phase(FP_TLS_HANDSHAKE); 812 + } else if (fctx.conn.error || fetch_timed_out()) { 813 + fetch_fail(-30); 814 + return NET_FETCH_ERROR; 815 + } 816 + break; 817 + 818 + case FP_TLS_HANDSHAKE: { 819 + int pump = tls_pump_io(); 820 + if (pump < 0) { fetch_fail(-600 - fctx.error_code); return NET_FETCH_ERROR; } 821 + unsigned st = br_ssl_engine_current_state(&fctx.sc->eng); 822 + if (st & BR_SSL_SENDAPP) { 823 + /* Handshake done — build HTTP/1.1 keep-alive request. Also 824 + * allocate the response buffer up-front so FP_HTTP_RECV 825 + * doesn't have to deal with a NULL check on first append. */ 826 + fetch_build_request(); 827 + fctx.resp_cap = 1 << 16; 828 + fctx.resp_buf = malloc(fctx.resp_cap); 829 + fctx.resp_len = 0; 830 + if (!fctx.resp_buf) { fetch_fail(-50); return NET_FETCH_ERROR; } 831 + fetch_set_phase(FP_HTTP_SEND); 832 + } else if (fctx.conn.error || fetch_timed_out()) { 833 + fetch_fail(-50); 834 + return NET_FETCH_ERROR; 835 + } 836 + break; 837 + } 838 + 839 + case FP_HTTP_SEND: { 840 + int pump = tls_pump_io(); 841 + if (pump < 0) { fetch_fail(-600 - fctx.error_code); return NET_FETCH_ERROR; } 842 + unsigned st = br_ssl_engine_current_state(&fctx.sc->eng); 843 + if ((st & BR_SSL_SENDAPP) && fctx.req_sent < fctx.req_len) { 844 + size_t len; 845 + unsigned char *buf = br_ssl_engine_sendapp_buf(&fctx.sc->eng, &len); 846 + size_t remain = fctx.req_len - fctx.req_sent; 847 + size_t chunk = remain < len ? remain : len; 848 + memcpy(buf, fctx.req + fctx.req_sent, chunk); 849 + br_ssl_engine_sendapp_ack(&fctx.sc->eng, chunk); 850 + fctx.req_sent += chunk; 851 + if (fctx.req_sent >= fctx.req_len) 852 + br_ssl_engine_flush(&fctx.sc->eng, 0); 853 + } 854 + if (fctx.req_sent >= fctx.req_len) { 855 + /* resp_buf was already allocated at the fresh-connect or 856 + * reuse path entry, so we can just transition. */ 857 + fetch_set_phase(FP_HTTP_RECV); 858 + } else if (fctx.conn.error || fetch_timed_out()) { 859 + fetch_fail(-55); 860 + return NET_FETCH_ERROR; 861 + } 862 + break; 863 + } 864 + 865 + case FP_HTTP_RECV: { 866 + /* Drive the TLS engine as far as possible within one tick. 867 + * 868 + * The previous implementation called tls_pump_io() exactly 869 + * once per net_fetch_tick, and HolyC calls net_fetch_tick 870 + * from a loop with Sleep(10) between iterations. That pinned 871 + * TLS throughput to roughly "one BearSSL I/O chunk per 10ms" 872 + * which in practice landed around 1-2 Mbps regardless of how 873 + * much data lwIP had queued in rxbuf or how big TCP_WND was — 874 + * we measured this as the real cap after ruling out BDP, 875 + * pbuf pools, and the NIC ring. 876 + * 877 + * The loop below pumps until a full pass through the engine 878 + * makes zero progress. A pass moves bytes in three possible 879 + * directions: 880 + * 1. rxbuf → engine (BearSSL recvrec) 881 + * 2. engine → resp_buf (decrypted app data) 882 + * 3. engine → TCP write (outbound records, handshake/alerts) 883 + * 884 + * We also call tos_netif_poll() inside the loop because new 885 + * pbufs keep arriving while we work — without it we'd drain 886 + * the snapshot captured at the top of net_fetch_tick and then 887 + * stop, even though more cipher might be sitting in the NIC 888 + * ring waiting to be lifted into rxbuf. iter_cap prevents a 889 + * pathological live-lock if the engine reports progress but 890 + * no bytes actually move. */ 891 + int pump = 0; 892 + int iter; 893 + for (iter = 0; iter < 64; iter++) { 894 + int progress = 0; 895 + 896 + tos_netif_poll(); 897 + 898 + size_t rxlen_before = fctx.conn.rxlen; 899 + pump = tls_pump_io(); 900 + if (pump < 0) break; 901 + if (fctx.conn.rxlen != rxlen_before) progress = 1; 902 + 903 + unsigned st = br_ssl_engine_current_state(&fctx.sc->eng); 904 + if (st & BR_SSL_RECVAPP) { 905 + size_t len; 906 + unsigned char *buf = 907 + br_ssl_engine_recvapp_buf(&fctx.sc->eng, &len); 908 + if (len > 0) { 909 + while (fctx.resp_len + len > fctx.resp_cap) { 910 + size_t newcap = fctx.resp_cap * 2; 911 + unsigned char *nb = malloc(newcap); 912 + if (!nb) { 913 + fetch_fail(-80); 914 + return NET_FETCH_ERROR; 915 + } 916 + memcpy(nb, fctx.resp_buf, fctx.resp_len); 917 + free(fctx.resp_buf); 918 + fctx.resp_buf = nb; 919 + fctx.resp_cap = newcap; 920 + } 921 + memcpy(fctx.resp_buf + fctx.resp_len, buf, len); 922 + fctx.resp_len += len; 923 + br_ssl_engine_recvapp_ack(&fctx.sc->eng, len); 924 + progress = 1; 925 + } 926 + } 927 + 928 + if (!progress) break; 929 + } 930 + 931 + unsigned st = br_ssl_engine_current_state(&fctx.sc->eng); 932 + 933 + /* Try to parse headers as soon as possible. Once we have them 934 + * we know Content-Length (if the server sent one), which lets 935 + * us early-exit as soon as the body is complete and reuse the 936 + * TLS connection for the next request. */ 937 + if (!fctx.headers_done && fctx.resp_len > 0) { 938 + int minor_version, status; 939 + const char *msg; 940 + size_t msg_len; 941 + struct phr_header headers[64]; 942 + size_t num_headers = 64; 943 + int hdr_len = phr_parse_response( 944 + (const char *)fctx.resp_buf, fctx.resp_len, 945 + &minor_version, &status, &msg, &msg_len, 946 + headers, &num_headers, 0); 947 + if (hdr_len > 0) { 948 + fctx.headers_done = 1; 949 + fctx.body_start = (size_t)hdr_len; 950 + fctx.content_length = 0; 951 + fctx.http_status = status; 952 + /* HTTP/1.1 defaults to keep-alive; HTTP/1.0 defaults 953 + * to close. Either can be overridden by an explicit 954 + * Connection: header in the response. */ 955 + fctx.keep_alive = (minor_version >= 1); 956 + for (size_t i = 0; i < num_headers; i++) { 957 + if (header_name_eq(headers[i].name, headers[i].name_len, 958 + "Content-Length")) { 959 + /* Inline decimal parse — our minilib doesn't 960 + * have strtoul. Tolerates leading whitespace. */ 961 + size_t cl = 0; 962 + int seen_digit = 0; 963 + for (size_t k = 0; k < headers[i].value_len; k++) { 964 + char c = headers[i].value[k]; 965 + if (c >= '0' && c <= '9') { 966 + cl = cl * 10 + (size_t)(c - '0'); 967 + seen_digit = 1; 968 + } else if (!seen_digit && (c == ' ' || c == '\t')) { 969 + continue; 970 + } else { 971 + break; 972 + } 973 + } 974 + fctx.content_length = cl; 975 + } else if (header_name_eq(headers[i].name, headers[i].name_len, 976 + "Connection")) { 977 + if (headers[i].value_len >= 5 && 978 + header_name_eq(headers[i].value, 5, "close")) { 979 + fctx.keep_alive = 0; 980 + } else if (headers[i].value_len >= 10 && 981 + header_name_eq(headers[i].value, 10, "keep-alive")) { 982 + fctx.keep_alive = 1; 983 + } 984 + } 985 + } 986 + } 987 + /* hdr_len == -2 means "need more data" — just keep reading. 988 + * hdr_len < -2 means malformed; we'll fall through to the 989 + * close-based EOF handler below. */ 990 + } 991 + 992 + /* Early-exit on Content-Length. This is the big win for 993 + * keep-alive: we stop reading as soon as the body is complete 994 + * and leave the TLS connection open for the next request. */ 995 + if (fctx.headers_done && fctx.content_length > 0) { 996 + size_t body_have = fctx.resp_len - fctx.body_start; 997 + if (body_have >= fctx.content_length) { 998 + /* Truncate any extra bytes we overread (shouldn't 999 + * happen unless the server sent extra without asking). */ 1000 + fctx.resp_len = fctx.body_start + fctx.content_length; 1001 + if (fctx.keep_alive) { 1002 + fctx.conn_valid = 1; 1003 + } else { 1004 + fetch_close_conn_full(); 1005 + } 1006 + fctx.phase = FP_DONE; 1007 + return NET_FETCH_DONE; 1008 + } 1009 + } 1010 + 1011 + /* TLS error — reject even if we have partial data */ 1012 + if (pump < 0) { 1013 + fetch_fail(-600 - fctx.error_code); 1014 + return NET_FETCH_ERROR; 1015 + } 1016 + /* Normal TLS close — accept if we have data. Fallback EOF 1017 + * path for servers that omit Content-Length and just close 1018 + * the connection when they're done. */ 1019 + if (pump == 1 || st == BR_SSL_CLOSED) { 1020 + if (fctx.resp_len > 0) { 1021 + fetch_close_conn_full(); 1022 + fctx.phase = FP_DONE; 1023 + return NET_FETCH_DONE; 1024 + } 1025 + if (retry_dead_keepalive()) return NET_FETCH_PENDING; 1026 + fetch_fail(-60); 1027 + return NET_FETCH_ERROR; 1028 + } 1029 + /* TCP closed, no buffered data left, no pending app data */ 1030 + if (fctx.conn.closed && fctx.conn.rxlen == 0 1031 + && !(st & BR_SSL_RECVAPP)) { 1032 + if (fctx.resp_len > 0) { 1033 + fetch_close_conn_full(); 1034 + fctx.phase = FP_DONE; 1035 + return NET_FETCH_DONE; 1036 + } 1037 + if (retry_dead_keepalive()) return NET_FETCH_PENDING; 1038 + fetch_fail(-60); 1039 + return NET_FETCH_ERROR; 1040 + } 1041 + if (fetch_timed_out()) { 1042 + fetch_fail(-60); 1043 + return NET_FETCH_ERROR; 1044 + } 1045 + break; 1046 + } 1047 + 1048 + default: 1049 + break; 1050 + } 1051 + 1052 + return NET_FETCH_PENDING; 1053 + } 1054 + 1055 + int net_fetch_result(net_response_t *resp) 1056 + { 1057 + if (fctx.phase == FP_ERROR) { 1058 + int code = fctx.error_code; 1059 + fctx.phase = FP_IDLE; 1060 + /* Error path: connection is already torn down by fetch_fail. */ 1061 + return code; 1062 + } 1063 + if (fctx.phase != FP_DONE) return -1; 1064 + 1065 + /* Headers were already parsed during FP_HTTP_RECV; reuse the 1066 + * cached offsets. If headers_done is 0 we're in the legacy 1067 + * close-based path (no Content-Length seen) and need to parse 1068 + * now, same as before the keep-alive refactor. */ 1069 + if (!fctx.headers_done) { 1070 + int minor_version, status; 1071 + const char *msg; 1072 + size_t msg_len; 1073 + struct phr_header headers[64]; 1074 + size_t num_headers = 64; 1075 + 1076 + int hdr_len = phr_parse_response( 1077 + (const char *)fctx.resp_buf, fctx.resp_len, 1078 + &minor_version, &status, &msg, &msg_len, 1079 + headers, &num_headers, 0); 1080 + 1081 + if (hdr_len < 0) { 1082 + free(fctx.resp_buf); fctx.resp_buf = NULL; 1083 + fctx.phase = FP_IDLE; 1084 + return -70; 1085 + } 1086 + fctx.http_status = status; 1087 + fctx.body_start = (size_t)hdr_len; 1088 + } 1089 + 1090 + resp->status = fctx.http_status; 1091 + resp->body_len = fctx.resp_len - fctx.body_start; 1092 + resp->body = malloc(resp->body_len); 1093 + if (!resp->body) { 1094 + free(fctx.resp_buf); fctx.resp_buf = NULL; 1095 + fctx.phase = FP_IDLE; 1096 + return -80; 1097 + } 1098 + memcpy(resp->body, fctx.resp_buf + fctx.body_start, resp->body_len); 1099 + 1100 + free(fctx.resp_buf); fctx.resp_buf = NULL; 1101 + fctx.resp_len = 0; 1102 + fctx.resp_cap = 0; 1103 + fctx.phase = FP_IDLE; 1104 + return 0; 1105 + } 1106 + 1107 + extern uint32_t tos_netif_get_ip(void); 1108 + static uint32_t net_get_ip(void) { return tos_netif_get_ip(); } 1109 + 1110 + /* ---- JSON API (backed by jsmn via json_wrap) ---- */ 1111 + 1112 + net_json_t net_json_parse(const char *data, size_t len) { 1113 + return (net_json_t)json_parse(data, len); } 1114 + void net_json_free(net_json_t j) { 1115 + json_free((json_handle_t *)j); } 1116 + const char *net_json_str(net_json_t j, const char *key) { 1117 + return json_str((json_handle_t *)j, key); } 1118 + net_json_t net_json_obj(net_json_t j, const char *key) { 1119 + return (net_json_t)json_obj((json_handle_t *)j, key); } 1120 + net_json_t net_json_arr(net_json_t j, const char *key) { 1121 + return (net_json_t)json_arr((json_handle_t *)j, key); } 1122 + int net_json_arr_len(net_json_t j) { 1123 + return json_arr_len((json_handle_t *)j); } 1124 + net_json_t net_json_arr_idx(net_json_t j, int idx) { 1125 + return (net_json_t)json_arr_idx((json_handle_t *)j, idx); } 1126 + int net_json_is_str(net_json_t j) { 1127 + return json_is_str((json_handle_t *)j); } 1128 + int net_json_is_arr(net_json_t j) { 1129 + return json_is_arr((json_handle_t *)j); } 1130 + int net_json_is_obj(net_json_t j) { 1131 + return json_is_obj((json_handle_t *)j); } 1132 + const char *net_json_strval(net_json_t j) { 1133 + return json_strval((json_handle_t *)j); } 1134 + 1135 + /* Bring up lwIP + the NIC. net_entry.c owns the actual api vtable 1136 + * handed back to HolyC; this call is just initialization. */ 1137 + int net_api_init(nic_send_fn send, nic_recv_fn recv, 1138 + const unsigned char *mac, size_t mac_len) 1139 + { 1140 + return tos_netif_init(send, recv, mac, mac_len); 1141 + }
+98
net/net_api.h
··· 1 + /* 2 + * net_api.h -- VodBrowser network library API 3 + * 4 + * Bundles BearSSL + picohttpparser + jsmn + lwIP behind a simple fetch/json 5 + * interface. This header defines the thunk boundary: HolyC calls these 6 + * functions via the function table returned by net_api_init(). 7 + */ 8 + #ifndef VODBROWSER_NET_API_H 9 + #define VODBROWSER_NET_API_H 10 + 11 + #include <stddef.h> 12 + #include <stdint.h> 13 + 14 + /* NIC driver callbacks (provided by HolyC, passed to init) */ 15 + typedef int (*nic_send_fn)(const unsigned char *frame, size_t len); 16 + typedef int (*nic_recv_fn)(unsigned char *buf, size_t maxlen); 17 + 18 + /* ---- Fetch API ---- */ 19 + 20 + typedef struct { 21 + int status; 22 + unsigned char *body; 23 + size_t body_len; 24 + } net_response_t; 25 + 26 + int net_fetch(const char *url, net_response_t *resp); 27 + int net_fetch_range(const char *url, size_t range_start, size_t range_end, 28 + net_response_t *resp); 29 + void net_free_response(net_response_t *resp); 30 + 31 + /* Async fetch API — non-blocking state machine for cooperative multitasking. 32 + * HolyC calls begin(), then loops tick()+Sleep() until done, then result(). */ 33 + #define NET_FETCH_IDLE 0 34 + #define NET_FETCH_PENDING 1 35 + #define NET_FETCH_DONE 2 36 + #define NET_FETCH_ERROR -1 37 + 38 + int net_fetch_begin(const char *url); 39 + int net_fetch_tick(void); 40 + int net_fetch_result(net_response_t *resp); 41 + 42 + /* Extra headers (e.g. Range) to attach to the next async fetch. Cleared 43 + * automatically once the request is built. */ 44 + void net_set_fetch_headers(const char *hdrs); 45 + 46 + /* Returns 1 if the most recent net_fetch_begin() reused an existing 47 + * TLS/TCP connection, 0 if it opened a fresh one. Diagnostic hook — 48 + * HolyC surfaces this in the per-fetch debug log. */ 49 + int net_get_last_reused(void); 50 + 51 + /* ---- JSON API (opaque handle) ---- */ 52 + 53 + typedef struct net_json *net_json_t; 54 + 55 + net_json_t net_json_parse(const char *data, size_t len); 56 + void net_json_free(net_json_t json); 57 + const char *net_json_str(net_json_t json, const char *key); 58 + net_json_t net_json_obj(net_json_t json, const char *key); 59 + net_json_t net_json_arr(net_json_t json, const char *key); 60 + int net_json_arr_len(net_json_t json); 61 + net_json_t net_json_arr_idx(net_json_t json, int idx); 62 + int net_json_is_str(net_json_t json); 63 + int net_json_is_arr(net_json_t json); 64 + int net_json_is_obj(net_json_t json); 65 + const char *net_json_strval(net_json_t json); 66 + 67 + /* ---- Function table for thunking ---- */ 68 + 69 + struct net_api { 70 + /* 0 */ int (*fetch)(const char *url, net_response_t *resp); 71 + /* 1 */ int (*fetch_range)(const char *url, size_t start, size_t end, net_response_t *resp); 72 + /* 2 */ void (*free_response)(net_response_t *resp); 73 + /* 3 */ net_json_t (*json_parse)(const char *data, size_t len); 74 + /* 4 */ void (*json_free)(net_json_t json); 75 + /* 5 */ const char *(*json_str)(net_json_t json, const char *key); 76 + /* 6 */ net_json_t (*json_obj)(net_json_t json, const char *key); 77 + /* 7 */ net_json_t (*json_arr)(net_json_t json, const char *key); 78 + /* 8 */ int (*json_arr_len)(net_json_t json); 79 + /* 9 */ net_json_t (*json_arr_idx)(net_json_t json, int idx); 80 + /* 10 */ int (*json_is_str)(net_json_t json); 81 + /* 11 */ int (*json_is_arr)(net_json_t json); 82 + /* 12 */ int (*json_is_obj)(net_json_t json); 83 + /* 13 */ const char *(*json_strval)(net_json_t json); 84 + /* 14 */ void (*poll)(void); /* drive lwIP timers / NIC */ 85 + /* 15 */ uint32_t (*get_ip)(void); /* 0 = no IP yet */ 86 + /* 16 */ int (*fetch_begin)(const char *url); 87 + /* 17 */ int (*fetch_tick)(void); 88 + /* 18 */ int (*fetch_result)(net_response_t *resp); 89 + /* 19 */ void (*set_fetch_headers)(const char *hdrs); 90 + /* 20 */ int (*get_last_reused)(void); 91 + }; 92 + 93 + /* Bring up lwIP + the NIC. Returns 0 on success. The actual api vtable 94 + * is owned by net_entry.c and handed to HolyC from elf_main. */ 95 + int net_api_init(nic_send_fn send, nic_recv_fn recv, 96 + const unsigned char *mac, size_t mac_len); 97 + 98 + #endif
+84
net/net_entry.c
··· 1 + /* 2 + * net_entry.c -- ELF entry point for Net.prg 3 + * 4 + * Historical note: this file used to switch %rsp to an 8MB heap buffer 5 + * via call_on_big_stack before every C call. That caused TempleOS 6 + * WallPaper to panic "Stk Overflow" when the timer IRQ preempted us 7 + * with rsp pointing into the heap — TempleOS's UnusedStk(task) check 8 + * requires rsp to be inside the task's official stack range. Fix: run 9 + * the C code directly on the caller's task stack. Callers MUST Spawn 10 + * NetTask with a large stk_size (4MB used by VodBrowser.HC). 11 + */ 12 + #include "net_api.h" 13 + #include <stddef.h> 14 + #include <stdint.h> 15 + 16 + /* Must match the layout HolyC Net.HC builds in NetInit(). */ 17 + struct net_init_args { 18 + nic_send_fn send; /* offset 0 */ 19 + nic_recv_fn recv; /* offset 8 */ 20 + unsigned char mac[6]; /* offset 16 */ 21 + unsigned char _pad[2]; 22 + uint64_t (*rand_u64)(void); /* offset 24 */ 23 + uint64_t (*get_ticks)(void); /* offset 32 */ 24 + void *(*tos_malloc)(uint64_t); /* offset 40 */ 25 + void (*tos_free)(void *); /* offset 48 */ 26 + }; 27 + 28 + extern void net_poll_internal(void); 29 + extern uint32_t tos_netif_get_ip(void); 30 + extern void net_set_fetch_headers(const char *hdrs); 31 + 32 + /* API vtable handed back to HolyC. All slots point directly at the 33 + * corresponding C function — no wrapping, no stack switching. */ 34 + static const struct net_api api = { 35 + .fetch = net_fetch, 36 + .fetch_range = net_fetch_range, 37 + .free_response = net_free_response, 38 + .json_parse = net_json_parse, 39 + .json_free = net_json_free, 40 + .json_str = net_json_str, 41 + .json_obj = net_json_obj, 42 + .json_arr = net_json_arr, 43 + .json_arr_len = net_json_arr_len, 44 + .json_arr_idx = net_json_arr_idx, 45 + .json_is_str = net_json_is_str, 46 + .json_is_arr = net_json_is_arr, 47 + .json_is_obj = net_json_is_obj, 48 + .json_strval = net_json_strval, 49 + .poll = net_poll_internal, 50 + .get_ip = tos_netif_get_ip, 51 + .fetch_begin = net_fetch_begin, 52 + .fetch_tick = net_fetch_tick, 53 + .fetch_result = net_fetch_result, 54 + .set_fetch_headers = net_set_fetch_headers, 55 + .get_last_reused = net_get_last_reused, 56 + }; 57 + 58 + void *elf_main(void *arg1, void *arg2) 59 + { 60 + (void)arg2; 61 + 62 + if (!arg1) { 63 + static const struct net_api dummy = {0}; 64 + return (void *)&dummy; 65 + } 66 + 67 + /* Wire minilib's malloc/free/rand/ticks to the TempleOS kernel 68 + * callbacks that HolyC passes in. */ 69 + extern void minilib_set_callbacks( 70 + uint64_t (*rand_fn)(void), 71 + uint64_t (*ticks_fn)(void), 72 + void *(*malloc_fn)(uint64_t), 73 + void (*free_fn)(void *)); 74 + 75 + struct net_init_args *args = arg1; 76 + minilib_set_callbacks(args->rand_u64, args->get_ticks, 77 + args->tos_malloc, args->tos_free); 78 + 79 + /* Bring up lwIP + NIC directly on our task stack. */ 80 + if (net_api_init(args->send, args->recv, args->mac, 6) < 0) 81 + return (void *)1; 82 + 83 + return (void *)&api; 84 + }
+121
net/tos_netif.c
··· 1 + /* 2 + * tos_netif.c — lwIP network interface for TempleOS 3 + * 4 + * Bridges lwIP to the HolyC NIC driver via function pointers. 5 + */ 6 + #include "tos_netif.h" 7 + 8 + #include "lwip/init.h" 9 + #include "lwip/netif.h" 10 + #include "lwip/etharp.h" 11 + #include "lwip/dhcp.h" 12 + #include "lwip/timeouts.h" 13 + #include "lwip/pbuf.h" 14 + #include "lwip/ip4_addr.h" 15 + #include "netif/ethernet.h" 16 + 17 + #include <string.h> 18 + 19 + static struct netif tos_netif; 20 + static nic_send_fn g_nic_send; 21 + static nic_recv_fn g_nic_recv; 22 + 23 + /* lwIP calls this to send an Ethernet frame */ 24 + static err_t tos_linkoutput(struct netif *netif, struct pbuf *p) 25 + { 26 + (void)netif; 27 + /* Flatten pbuf chain into contiguous buffer */ 28 + unsigned char buf[1536]; 29 + if (p->tot_len > sizeof(buf)) return ERR_BUF; 30 + 31 + pbuf_copy_partial(p, buf, p->tot_len, 0); 32 + 33 + if (g_nic_send(buf, p->tot_len) < 0) 34 + return ERR_IF; 35 + 36 + return ERR_OK; 37 + } 38 + 39 + /* Called during init to set up the interface */ 40 + static err_t tos_netif_init_cb(struct netif *netif) 41 + { 42 + netif->name[0] = 't'; 43 + netif->name[1] = '0'; 44 + netif->output = etharp_output; 45 + netif->linkoutput = tos_linkoutput; 46 + netif->mtu = 1500; 47 + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; 48 + 49 + return ERR_OK; 50 + } 51 + 52 + /* Poll for incoming packets */ 53 + void tos_netif_poll(void) 54 + { 55 + unsigned char buf[1536]; 56 + int len; 57 + 58 + /* Check for received frames (limit per poll to avoid infinite loop 59 + * from broadcast traffic or NIC descriptor issues) */ 60 + int count = 0; 61 + while (count < 16 && (len = g_nic_recv(buf, sizeof(buf))) > 0) { 62 + struct pbuf *p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL); 63 + if (p) { 64 + pbuf_take(p, buf, (u16_t)len); 65 + if (tos_netif.input(p, &tos_netif) != ERR_OK) 66 + pbuf_free(p); 67 + } 68 + count++; 69 + } 70 + 71 + /* Process lwIP timers (TCP retransmit, ARP, DHCP, etc.) */ 72 + sys_check_timeouts(); 73 + } 74 + 75 + struct netif *tos_netif_get(void) 76 + { 77 + return &tos_netif; 78 + } 79 + 80 + /* Return current IP address as a 32-bit value (network byte order) */ 81 + uint32_t tos_netif_get_ip(void) 82 + { 83 + return ip4_addr_get_u32(netif_ip4_addr(&tos_netif)); 84 + } 85 + 86 + int tos_netif_init(nic_send_fn send, nic_recv_fn recv, 87 + const unsigned char *mac, size_t mac_len) 88 + { 89 + ip4_addr_t ipaddr, netmask, gw; 90 + 91 + g_nic_send = send; 92 + g_nic_recv = recv; 93 + 94 + /* Initialize lwIP */ 95 + lwip_init(); 96 + 97 + /* Start with 0.0.0.0 — DHCP will assign an address */ 98 + ip4_addr_set_zero(&ipaddr); 99 + ip4_addr_set_zero(&netmask); 100 + ip4_addr_set_zero(&gw); 101 + 102 + /* Add network interface */ 103 + if (!netif_add(&tos_netif, &ipaddr, &netmask, &gw, 104 + NULL, tos_netif_init_cb, ethernet_input)) { 105 + return -1; 106 + } 107 + 108 + /* Set MAC address */ 109 + if (mac_len > sizeof(tos_netif.hwaddr)) 110 + mac_len = sizeof(tos_netif.hwaddr); 111 + memcpy(tos_netif.hwaddr, mac, mac_len); 112 + tos_netif.hwaddr_len = (u8_t)mac_len; 113 + 114 + netif_set_default(&tos_netif); 115 + netif_set_up(&tos_netif); 116 + 117 + /* Start DHCP to get an IP address */ 118 + dhcp_start(&tos_netif); 119 + 120 + return 0; 121 + }
+30
net/tos_netif.h
··· 1 + /* 2 + * tos_netif.h — lwIP network interface backed by HolyC NIC driver 3 + * 4 + * The NIC driver in HolyC provides two function pointers: 5 + * nic_send(frame, len) — send a raw Ethernet frame 6 + * nic_recv(buf, maxlen) — receive a raw Ethernet frame (returns len, 0 if none) 7 + * 8 + * These are passed to net_api_init() and registered here. 9 + */ 10 + #ifndef TOS_NETIF_H 11 + #define TOS_NETIF_H 12 + 13 + #include <stdint.h> 14 + #include <stddef.h> 15 + 16 + /* Function pointer types matching the HolyC NIC driver */ 17 + typedef int (*nic_send_fn)(const unsigned char *frame, size_t len); 18 + typedef int (*nic_recv_fn)(unsigned char *buf, size_t maxlen); 19 + 20 + /* Initialize the lwIP stack and register the NIC driver callbacks */ 21 + int tos_netif_init(nic_send_fn send, nic_recv_fn recv, 22 + const unsigned char *mac, size_t mac_len); 23 + 24 + /* Poll for incoming packets — call this periodically */ 25 + void tos_netif_poll(void); 26 + 27 + /* Get the network interface (for lwIP raw API) */ 28 + struct netif *tos_netif_get(void); 29 + 30 + #endif
+205
qemu-run.sh
··· 1 + #!/bin/bash 2 + # qemu-run.sh — Launch TempleOS in QEMU (cross-platform, no UTM needed) 3 + # 4 + # Usage: 5 + # ./qemu-run.sh Boot with sources.iso as CD 6 + # ./qemu-run.sh --install Install TempleOS from ISO 7 + # ./qemu-run.sh --cdrom other.iso Attach a specific CD 8 + # ./qemu-run.sh --no-cd Boot without CD 9 + # 10 + # First time: 11 + # ./qemu-run.sh --install 12 + # (say Y to everything, shut down, then: ./qemu-run.sh) 13 + # 14 + # Hardware profile is modeled on the UTM config we actually develop 15 + # against: 4-core Skylake-Client, 2 GB RAM, cirrus VGA, pcnet NIC, 16 + # IDE disk + IDE CD, TCG accel with a generous 512 MB translation 17 + # block cache. All of the SPICE / virtio-serial / vdagent / guest- 18 + # agent flags from the UTM invocation are dropped here — they only 19 + # make sense inside UTM's display pipeline and are useless for a 20 + # plain-SDL run. 21 + 22 + set -e 23 + 24 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 25 + ISO_DIR="$SCRIPT_DIR/isos" 26 + DISK="${SCRIPT_DIR}/templeos-disk.qcow2" 27 + TOS_ISO="$ISO_DIR/TempleOS.iso" 28 + SOURCES_ISO="$ISO_DIR/sources.iso" 29 + CDROM="$SOURCES_ISO" 30 + BOOT_ORDER="c" 31 + 32 + # Parse args 33 + while [ $# -gt 0 ]; do 34 + case "$1" in 35 + --install) 36 + CDROM="$TOS_ISO" 37 + BOOT_ORDER="d" 38 + shift 39 + ;; 40 + --cdrom) 41 + CDROM="$2" 42 + shift 2 43 + ;; 44 + --no-cd) 45 + CDROM="" 46 + shift 47 + ;; 48 + *) 49 + echo "Usage: $0 [--install | --cdrom ISO | --no-cd]" 50 + exit 1 51 + ;; 52 + esac 53 + done 54 + 55 + # Ensure TempleOS ISO exists if we need it for install 56 + if [ "$CDROM" = "$TOS_ISO" ] && [ ! -f "$TOS_ISO" ]; then 57 + echo "TempleOS ISO not found at $TOS_ISO" 58 + read -p "Download TOS_Distro.ISO? [Y/n] " yn 59 + case "$yn" in 60 + [Nn]*) echo "Place it manually at $TOS_ISO"; exit 1 ;; 61 + *) 62 + echo "Downloading..." 63 + curl -L -o "$TOS_ISO" "https://templeos.org/Downloads/TOS_Distro.ISO" 64 + ;; 65 + esac 66 + fi 67 + 68 + # Build sources.iso if missing 69 + if [ "$CDROM" = "$SOURCES_ISO" ] && [ ! -f "$SOURCES_ISO" ]; then 70 + echo "sources.iso not found, running deploy.sh..." 71 + "${SCRIPT_DIR}/deploy.sh" 72 + fi 73 + 74 + # Create disk if it doesn't exist. 4GB gives enough headroom for the 75 + # TempleOS install plus the home directory we write prefs into. 76 + if [ ! -f "$DISK" ]; then 77 + echo "Creating 4GB disk image..." 78 + qemu-img create -f qcow2 "$DISK" 4G 79 + fi 80 + 81 + # Detect an empty / never-installed disk. qcow2 is sparse: a freshly 82 + # created 4GB image occupies ~200KB on disk (just the header + 83 + # refcount table). After a TempleOS install the on-disk size jumps 84 + # to ~100MB+. So anything under 1MB is reliably "no OS installed." 85 + # 86 + # sources.iso is a data ISO (RedSea FS holding our .HC/.prg files 87 + # with boot code extracted from TempleOS.iso) — the boot code expects 88 + # a full TempleOS kernel somewhere, which sources.iso does NOT 89 + # contain. If we booted with sources.iso as CD1 on an empty disk the 90 + # user would see "not a bootable disk" followed by a failed load. 91 + # So force them into install mode instead, where CD1 is the real 92 + # TempleOS.iso and boot order is CD-first. 93 + DISK_BYTES="$(wc -c < "$DISK" | tr -d ' ')" 94 + if [ "$DISK_BYTES" -lt 1048576 ] && [ "$CDROM" != "$TOS_ISO" ]; then 95 + echo "" 96 + echo "Note: $DISK is empty (${DISK_BYTES} bytes on disk)." 97 + echo " Switching to --install mode so you can install TempleOS" 98 + echo " before booting from it." 99 + echo "" 100 + echo " Run 'y' through the TempleOS install prompts. When the" 101 + echo " installer finishes and the VM reboots, shut it down" 102 + echo " with Ctrl-Alt-Q (or close the window), then rerun" 103 + echo " ./qemu-run.sh normally to boot from the new HD install." 104 + echo "" 105 + CDROM="$TOS_ISO" 106 + BOOT_ORDER="d" 107 + fi 108 + 109 + # Pick a display backend that QEMU actually has compiled in. Homebrew 110 + # QEMU on macOS typically ships with cocoa but no sdl/gtk; most Linux 111 + # QEMU builds have gtk and sdl but no cocoa. Probing keeps the script 112 + # portable across both without forcing a hard dependency on SDL. 113 + DISPLAY_CHOICES="cocoa gtk sdl curses" 114 + DISPLAY_BACKEND="" 115 + AVAILABLE_DISPLAYS="$(qemu-system-x86_64 -display help 2>/dev/null || true)" 116 + for candidate in $DISPLAY_CHOICES; do 117 + if echo "$AVAILABLE_DISPLAYS" | /usr/bin/grep -q "^${candidate}\$"; then 118 + DISPLAY_BACKEND="$candidate" 119 + break 120 + fi 121 + done 122 + if [ -z "$DISPLAY_BACKEND" ]; then 123 + echo "WARN: no graphical QEMU display backend found (tried: $DISPLAY_CHOICES)" 124 + echo " falling back to curses — keyboard only, no graphics" 125 + DISPLAY_BACKEND="curses" 126 + fi 127 + 128 + QEMU_CMD=( 129 + qemu-system-x86_64 130 + 131 + # Machine + CPU. Pinning to pc-i440fx-10.0 rather than the generic 132 + # "pc" alias keeps the virtual hardware layout stable across QEMU 133 + # upgrades — we rely on specific device locations. vmport=off 134 + # disables the VMware backdoor (we're not VMware) and hpet=off 135 + # removes the high-precision event timer, which TempleOS doesn't 136 + # use and which measurably slows timer IRQs under TCG. 137 + -machine pc-i440fx-10.0,vmport=off,hpet=off 138 + 139 + # Skylake-Client matches the UTM config and exposes the instruction 140 + # set we compiled openh264 and BearSSL against (x86_64 baseline, 141 + # no SSE/AES-NI — those are disabled at compile time for TempleOS). 142 + -cpu Skylake-Client 143 + 144 + # 2 GB RAM — plenty for TempleOS (which tops out at a few hundred 145 + # MB of working set) plus our ring of 8 MB HLS segment slots. 146 + -m 2048 147 + 148 + # 4-core SMP. TempleOS V5.03 supports multiprocessor out of the 149 + # box; VodBrowser runs NetTask on core 1 and DecodeTask on core 2 150 + # (see CorePolicyBuild in Prefs.HC) so we want at least 3 cores. 151 + -smp cpus=4,sockets=1,cores=4,threads=1 152 + 153 + # TCG (pure software emulation) since KVM isn't available on 154 + # non-Linux hosts and TempleOS is sensitive enough that HVF / 155 + # WHPX have historically caused boot hangs. tb-size=512 gives the 156 + # JIT a 512 MB translation block cache — the default 32 MB is 157 + # routinely blown through by openh264 hot loops. 158 + -accel tcg,tb-size=512 159 + 160 + # Display — cirrus is the most compatible VGA for TempleOS's 161 + # graphics init. The display backend is chosen above based on 162 + # what the current QEMU build supports (cocoa/gtk/sdl/curses). 163 + -device cirrus-vga 164 + -display "$DISPLAY_BACKEND" 165 + 166 + # PS/2 keyboard + mouse. TempleOS requires PS/2; i440fx includes 167 + # i8042 by default so we don't need to add it explicitly. 168 + 169 + # Hard disk on IDE 170 + -drive "if=none,id=drive0,media=disk,file=${DISK},format=qcow2,discard=unmap,detect-zeroes=unmap" 171 + -device ide-hd,bus=ide.0,drive=drive0,bootindex=0 172 + 173 + # Network — PCnet-PCI II (Am79C970A). Our lwIP netstack drives 174 + # this chip via the Nic.HC driver; RTL8139 works too but the 175 + # openh264-builds we're developing against target pcnet. 176 + -device pcnet,netdev=net0 177 + -netdev user,id=net0 178 + 179 + # Boot order 180 + -boot "order=${BOOT_ORDER}" 181 + ) 182 + 183 + # CD-ROM on the second IDE channel, if specified. 184 + # 185 + # file.locking=off mirrors what UTM uses in its own invocation — 186 + # without it QEMU refuses to open an ISO another process (UTM, a 187 + # second QEMU instance, Finder preview) may already have mapped. 188 + # The ISO is read-only anyway so there's nothing to protect. 189 + if [ -n "$CDROM" ]; then 190 + QEMU_CMD+=( 191 + -drive "if=none,id=drive_cd,media=cdrom,file=${CDROM},readonly=on,file.locking=off" 192 + -device ide-cd,bus=ide.1,drive=drive_cd,bootindex=1 193 + ) 194 + fi 195 + 196 + echo "Launching TempleOS (VodBrowser)..." 197 + echo " Disk: ${DISK}" 198 + echo " CD: ${CDROM:-none}" 199 + echo " Boot: ${BOOT_ORDER}" 200 + echo " Display: ${DISPLAY_BACKEND}" 201 + echo "" 202 + echo "Keys: Ctrl+Alt+G to release mouse grab" 203 + echo "" 204 + 205 + "${QEMU_CMD[@]}"
screenshot.png

This is a binary file and will not be displayed.

screenshot2.png

This is a binary file and will not be displayed.

screenshot3.png

This is a binary file and will not be displayed.

tools/bootcode.bin

This is a binary file and will not be displayed.

+625
tools/mkdistroiso.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + mkdistroiso.py — Produce a bootable TempleOS ISO with VodBrowser 4 + pre-installed and set to auto-launch on boot. 5 + 6 + Strategy: INJECTION, not rewrite. The stock TempleOS V5.03 distro 7 + ISO is kept byte-for-byte identical in its existing sectors — every 8 + file in /Adam, /Kernel, /Compiler, /Home, /0000Boot stays at its 9 + original 512-byte sector offset. This matters because the stage-1 10 + boot loader at CD sector 21 is built against the stock layout; when 11 + we moved sectors around in an earlier rewrite-from-scratch attempt 12 + the result hung at "Loading TempleOS..." even though the file tree 13 + was valid — the bootcode evidently has baked-in sector assumptions. 14 + 15 + What we do: 16 + 1. Copy the stock ISO bytes verbatim. 17 + 2. Append new 512-byte sectors at the tail for our content: 18 + - a new /VodBrowser/ directory (holds ~20 entries) 19 + - every VodBrowser .HC / .prg file 20 + - a plain /HomeSys.HC at the root level 21 + 3. Patch two empty slots in the existing root directory with 22 + entries pointing at /VodBrowser/ and /HomeSys.HC. The stock 23 + root dir has exactly 2 trailing empty slots, so we don't need 24 + to grow or relocate any existing data. 25 + 4. Update the boot sector's total_sects field and the ISO9660 26 + PVD volume_space_size field to reflect the new, larger ISO. 27 + 28 + The reader (read_redsea / _read_dir / Node) is still here because 29 + the CLI uses it to sanity-check the base ISO before patching and to 30 + confirm that the root directory has enough slack. The full 31 + rebuild-from-scratch path is gone — it was a nice thought but the 32 + bootcode won't cooperate. 33 + 34 + Usage: 35 + mkdistroiso.py --base isos/TempleOS.iso \ 36 + --output isos/vodbrowser-dist.iso \ 37 + <path>[:<iso-name>] ... 38 + 39 + Each positional argument is a host path for a VodBrowser file. An 40 + optional ":name" suffix overrides the name used inside the ISO 41 + (default: basename of the host path). All VodBrowser files are 42 + installed under /VodBrowser/ in the output ISO, next to a plain 43 + /HomeSys.HC that launches VodBrowser on every login. 44 + """ 45 + 46 + from __future__ import annotations 47 + import argparse 48 + import os 49 + import struct 50 + import sys 51 + 52 + # ---- RedSea layout constants (matching mkredseaiso.py) ---- 53 + SECT = 512 54 + CD_SECT = 2048 55 + DIR_ENTRY = 64 56 + BOOT_SECT_NUM = 88 57 + BITMAP_SECTS = 9 58 + # Root directory lands at CD-sector boundary 100 (= 512-byte sector 100). 59 + # The base ISO's root sits at 98; bumping to 100 in our output keeps 60 + # it aligned to a 4-sector CD boundary. TempleOS reads the root cluster 61 + # from the PVD so the concrete value doesn't matter as long as we're 62 + # consistent. 63 + ROOT_CLUS_OUT = 100 64 + 65 + RS_ATTR_DIR = 0x0810 66 + RS_ATTR_FILE = 0x0C00 67 + 68 + 69 + # ================================================================== 70 + # Reader — walks a RedSea ISO into an in-memory tree. 71 + # ================================================================== 72 + 73 + 74 + class Node: 75 + """One file or directory in the tree. For files, `data` holds the 76 + raw bytes; for directories, `children` holds the child nodes.""" 77 + 78 + __slots__ = ("name", "attr", "data", "children", "datetime", "clus", "size") 79 + 80 + def __init__(self, name: str, attr: int, *, data: bytes | None = None, 81 + datetime: bytes = b"\x00" * 8): 82 + self.name = name 83 + self.attr = attr 84 + self.data = data 85 + self.children: list[Node] = [] 86 + self.datetime = datetime 87 + # Filled during layout (writer phase) 88 + self.clus = 0 89 + self.size = 0 90 + 91 + @property 92 + def is_dir(self) -> bool: 93 + return (self.attr & 0x0010) != 0 94 + 95 + 96 + def _read_dir(image: bytes, clus: int, parent: Node) -> None: 97 + """Populate parent.children by parsing the directory at `clus`.""" 98 + base = clus * SECT 99 + # The first entry ('.') has the directory's own total size in bytes 100 + # at offset 48. That tells us how many 64-byte entries to iterate. 101 + self_entry = image[base : base + DIR_ENTRY] 102 + dir_size = struct.unpack_from("<Q", self_entry, 48)[0] 103 + parent.clus = clus 104 + parent.size = dir_size 105 + if dir_size == 0: 106 + return 107 + 108 + # Skip the first two entries ('.' and '..'), then walk until either 109 + # we've consumed dir_size bytes or an entry with attr == 0 marks 110 + # the end. 111 + for i in range(2, dir_size // DIR_ENTRY): 112 + off = base + i * DIR_ENTRY 113 + entry = image[off : off + DIR_ENTRY] 114 + attr = struct.unpack_from("<H", entry, 0)[0] 115 + if attr == 0: 116 + break 117 + name = entry[2:40].split(b"\x00", 1)[0].decode("ascii", errors="replace") 118 + child_clus = struct.unpack_from("<Q", entry, 40)[0] 119 + child_size = struct.unpack_from("<Q", entry, 48)[0] 120 + datetime = bytes(entry[56:64]) 121 + 122 + if attr & 0x0010: # directory bit 123 + child = Node(name, attr, datetime=datetime) 124 + child.clus = child_clus 125 + child.size = child_size 126 + _read_dir(image, child_clus, child) 127 + else: 128 + child = Node(name, attr, 129 + data=bytes(image[child_clus * SECT : 130 + child_clus * SECT + child_size]), 131 + datetime=datetime) 132 + child.clus = child_clus 133 + child.size = child_size 134 + 135 + parent.children.append(child) 136 + 137 + 138 + def read_redsea(path: str) -> tuple[bytes, Node, bytes]: 139 + """Parse a RedSea ISO into (raw_image, root_node, bootcode). 140 + 141 + bootcode is the 2048-byte blob at CD sector 21 — the stage 1 142 + loader the real TempleOS installer puts there. Our output reuses 143 + whatever the input had so we don't have to extract bootcode 144 + separately. 145 + """ 146 + with open(path, "rb") as f: 147 + image = f.read() 148 + 149 + # Verify this looks like a TempleOS RedSea ISO. 150 + pvd = image[16 * CD_SECT : 17 * CD_SECT] 151 + if pvd[:6] != b"\x01CD001": 152 + raise ValueError(f"{path}: not an ISO9660 PVD (got {pvd[:6]!r})") 153 + if b"TempleOS RedSea" not in pvd[314:340]: 154 + raise ValueError(f"{path}: PVD publisher doesn't mark this as a " 155 + "TempleOS RedSea ISO — is this really a TempleOS " 156 + "distro ISO?") 157 + 158 + # RedSea boot sector at 512-byte sector 88 has the root cluster. 159 + bs = image[BOOT_SECT_NUM * SECT : BOOT_SECT_NUM * SECT + SECT] 160 + if bs[3] != 0x88: 161 + raise ValueError(f"{path}: missing RedSea signature at sector {BOOT_SECT_NUM}") 162 + root_clus = struct.unpack_from("<Q", bs, 24)[0] 163 + 164 + root = Node("", RS_ATTR_DIR) 165 + _read_dir(image, root_clus, root) 166 + 167 + bootcode = image[21 * CD_SECT : 22 * CD_SECT] 168 + return image, root, bootcode 169 + 170 + 171 + # ================================================================== 172 + # Tree operations. 173 + # ================================================================== 174 + 175 + 176 + def find_child(parent: Node, name: str) -> Node | None: 177 + for c in parent.children: 178 + if c.name == name: 179 + return c 180 + return None 181 + 182 + 183 + def ensure_dir(parent: Node, name: str) -> Node: 184 + existing = find_child(parent, name) 185 + if existing is not None: 186 + if not existing.is_dir: 187 + raise ValueError(f"/{name}: expected directory, found file") 188 + return existing 189 + new = Node(name, RS_ATTR_DIR) 190 + parent.children.append(new) 191 + return new 192 + 193 + 194 + def add_file(parent: Node, name: str, data: bytes) -> None: 195 + # Replace on conflict so repeated runs don't explode the directory. 196 + for i, c in enumerate(parent.children): 197 + if c.name == name: 198 + parent.children[i] = Node(name, RS_ATTR_FILE, data=data) 199 + return 200 + parent.children.append(Node(name, RS_ATTR_FILE, data=data)) 201 + 202 + 203 + # ================================================================== 204 + # Injection writer — take stock ISO bytes verbatim, append new 205 + # content + patch root directory slack slots in place. 206 + # ================================================================== 207 + 208 + 209 + def _serialize_dir_entry(name: str, attr: int, clus: int, size: int, 210 + datetime: bytes = b"\x00" * 8) -> bytes: 211 + entry = bytearray(DIR_ENTRY) 212 + struct.pack_into("<H", entry, 0, attr) 213 + nb = name.encode("ascii")[:37] 214 + entry[2 : 2 + len(nb)] = nb 215 + struct.pack_into("<Q", entry, 40, clus) 216 + struct.pack_into("<Q", entry, 48, size) 217 + entry[56:64] = datetime 218 + return bytes(entry) 219 + 220 + 221 + def _find_root_slack(image: bytes, root_clus: int) -> tuple[int, int]: 222 + """Return (used_count, total_slots) for the root directory. 223 + 224 + A 'slack' slot is one with attr == 0 (never written, or end-of- 225 + directory marker). The root '.' entry's `size` field gives the 226 + total byte capacity of the dir; we iterate 64-byte entries up 227 + to that limit and count used / free. 228 + """ 229 + base = root_clus * SECT 230 + first = image[base : base + DIR_ENTRY] 231 + dir_size = struct.unpack_from("<Q", first, 48)[0] 232 + total = dir_size // DIR_ENTRY 233 + used = 0 234 + for i in range(total): 235 + e = image[base + i * DIR_ENTRY : base + (i + 1) * DIR_ENTRY] 236 + attr = struct.unpack_from("<H", e, 0)[0] 237 + if attr == 0: 238 + break 239 + used += 1 240 + return used, total 241 + 242 + 243 + def _find_entry_by_name(image: bytes, dir_clus: int, 244 + name: str) -> int | None: 245 + """Scan a directory and return the 0-based slot of the entry whose 246 + name matches, or None if not found. Used to locate specific stock 247 + files (e.g. HomeSys.HC.Z) for in-place rewriting.""" 248 + base = dir_clus * SECT 249 + first = image[base : base + DIR_ENTRY] 250 + dir_size = struct.unpack_from("<Q", first, 48)[0] 251 + total = dir_size // DIR_ENTRY 252 + target = name.encode("ascii") 253 + for i in range(total): 254 + e = image[base + i * DIR_ENTRY : base + (i + 1) * DIR_ENTRY] 255 + attr = struct.unpack_from("<H", e, 0)[0] 256 + if attr == 0: 257 + continue 258 + entry_name = bytes(e[2:40]).split(b"\x00", 1)[0] 259 + if entry_name == target: 260 + return i 261 + return None 262 + 263 + 264 + def inject_distro(base_path: str, 265 + vod_files: list[tuple[str, bytes]], 266 + homesys_hc: bytes, 267 + output_path: str) -> None: 268 + """Build output_path by appending new content to base_path and 269 + patching the root directory in place. 270 + 271 + Adds a new /VodBrowser/ directory (in the first free root-dir 272 + slot) and rewrites the existing /HomeSys.HC.Z entry to point at 273 + a plain-text /HomeSys.HC containing our auto-launch snippet: 274 + 275 + - We walk the root directory to find the slot holding the stock 276 + /HomeSys.HC.Z entry, rename it to "HomeSys.HC" in place, and 277 + update the size field. The file's cluster stays where it was 278 + so no other sectors move. 279 + - We overwrite the data at that cluster with homesys_hc plus 280 + zero padding out to the original allocation. TempleOS reads 281 + plain ".HC" files without decompression, so our launcher runs 282 + as-is on every login. 283 + 284 + This approach is layout-neutral: we consume exactly one new 285 + root-dir slot (for /VodBrowser/) and reuse one existing slot 286 + (HomeSys.HC.Z → HomeSys.HC). Slot 23 stays as the zero-attr 287 + terminator, which the in-kernel directory reader needs. 288 + """ 289 + with open(base_path, "rb") as f: 290 + image = bytearray(f.read()) 291 + 292 + # Verify magic and pull the root cluster + total_sects. 293 + pvd = bytes(image[16 * CD_SECT : 17 * CD_SECT]) 294 + if pvd[:6] != b"\x01CD001": 295 + raise ValueError(f"{base_path}: not an ISO9660 PVD") 296 + if b"TempleOS RedSea" not in pvd[314:340]: 297 + raise ValueError(f"{base_path}: not a TempleOS RedSea ISO") 298 + 299 + bs = bytes(image[BOOT_SECT_NUM * SECT : BOOT_SECT_NUM * SECT + SECT]) 300 + if bs[3] != 0x88: 301 + raise ValueError(f"{base_path}: missing RedSea boot signature") 302 + root_clus = struct.unpack_from("<Q", bs, 24)[0] 303 + old_total_sects = struct.unpack_from("<Q", bs, 16)[0] 304 + 305 + # Check root-dir slack. We only need one slot for /VodBrowser/ — 306 + # we intentionally leave the last slack slot as a zero terminator 307 + # because the kernel's directory scanner (or at least some code 308 + # path hit during UserStartUp) crashes when there's no trailing 309 + # zero-attr entry. Filling both slots made TempleOS page-fault 310 + # with "LBts(&Fs->display_fl)" on first boot. 311 + used, total = _find_root_slack(image, root_clus) 312 + free = total - used 313 + if free < 2: 314 + raise ValueError( 315 + f"root directory only has {free} free slots " 316 + f"({used}/{total} used) — need at least 1 for " 317 + f"/VodBrowser/ plus 1 zero-attr terminator") 318 + 319 + # Find the stock /HomeSys.HC.Z entry so we can rewrite it in 320 + # place as a plain /HomeSys.HC. This is how we get auto-launch 321 + # without consuming a second root-dir slot. 322 + homesys_slot = _find_entry_by_name(image, root_clus, "HomeSys.HC.Z") 323 + if homesys_slot is None: 324 + raise ValueError("base ISO has no /HomeSys.HC.Z entry in root " 325 + "directory — can't install launcher in place") 326 + homesys_off = root_clus * SECT + homesys_slot * DIR_ENTRY 327 + homesys_entry = image[homesys_off : homesys_off + DIR_ENTRY] 328 + old_homesys_clus = struct.unpack_from("<Q", homesys_entry, 40)[0] 329 + old_homesys_size = struct.unpack_from("<Q", homesys_entry, 48)[0] 330 + # The stock file is ~561 bytes = one sector of allocation. Our 331 + # launcher needs to fit inside that single sector so we don't 332 + # stomp the next file's data. 333 + homesys_alloc_sects = max(1, (old_homesys_size + SECT - 1) // SECT) 334 + homesys_cap = homesys_alloc_sects * SECT 335 + if len(homesys_hc) > homesys_cap: 336 + raise ValueError( 337 + f"launcher is {len(homesys_hc)} bytes but the in-place " 338 + f"HomeSys.HC.Z allocation is only {homesys_cap} bytes") 339 + 340 + # ---------------------------------------------------------------- 341 + # Plan the appended layout. "start_sect" is the first 512-byte 342 + # sector past the end of the stock ISO — everything we add goes 343 + # there and grows the file. 344 + # ---------------------------------------------------------------- 345 + start_sect = len(image) // SECT 346 + if len(image) % SECT: 347 + # Pad to a 512-byte boundary first — our appended content is 348 + # sector-addressed. 349 + pad = SECT - (len(image) % SECT) 350 + image.extend(b"\x00" * pad) 351 + start_sect = len(image) // SECT 352 + 353 + # /VodBrowser/ directory layout: 354 + # entry 0: '.' (self) 355 + # entry 1: '..' (root) 356 + # entry 2..: one per file 357 + num_vod_entries = 2 + len(vod_files) 358 + vod_dir_bytes = num_vod_entries * DIR_ENTRY 359 + vod_dir_sects = max(1, (vod_dir_bytes + SECT - 1) // SECT) 360 + # Round up to a multiple of the directory-entry size so we don't 361 + # leave a garbage tail. Padding to sector is enough. 362 + vod_dir_size_field = vod_dir_sects * SECT 363 + 364 + vod_dir_clus = start_sect 365 + cur = vod_dir_clus + vod_dir_sects 366 + 367 + # Allocate sectors for each VodBrowser file 368 + vod_file_plans = [] 369 + for name, data in vod_files: 370 + sects = max(1, (len(data) + SECT - 1) // SECT) 371 + vod_file_plans.append((name, data, cur, sects)) 372 + cur += sects 373 + 374 + new_end_sect = cur 375 + # Round up to a CD sector (2048-byte / 4 × 512-byte) boundary so 376 + # total_cd_sects is clean. 377 + while new_end_sect % 4 != 0: 378 + new_end_sect += 1 379 + 380 + # Extend image buffer to cover everything we're about to write. 381 + needed_bytes = new_end_sect * SECT 382 + if needed_bytes > len(image): 383 + image.extend(b"\x00" * (needed_bytes - len(image))) 384 + 385 + # ---------------------------------------------------------------- 386 + # Write the new content. 387 + # ---------------------------------------------------------------- 388 + 389 + # /VodBrowser/ directory: '.' and '..' then one entry per file. 390 + off = vod_dir_clus * SECT 391 + image[off : off + DIR_ENTRY] = _serialize_dir_entry( 392 + ".", RS_ATTR_DIR, vod_dir_clus, vod_dir_size_field) 393 + image[off + DIR_ENTRY : off + 2 * DIR_ENTRY] = _serialize_dir_entry( 394 + "..", RS_ATTR_DIR, root_clus, 0) 395 + for i, (name, data, file_clus, file_sects) in enumerate(vod_file_plans): 396 + e_off = off + (i + 2) * DIR_ENTRY 397 + image[e_off : e_off + DIR_ENTRY] = _serialize_dir_entry( 398 + name, RS_ATTR_FILE, file_clus, len(data)) 399 + 400 + # Each VodBrowser file's raw bytes 401 + for name, data, file_clus, file_sects in vod_file_plans: 402 + f_off = file_clus * SECT 403 + image[f_off : f_off + len(data)] = data 404 + 405 + # ---------------------------------------------------------------- 406 + # Patch the root directory: 407 + # - Fill ONE slack slot with the new /VodBrowser/ entry. 408 + # - Rewrite the existing /HomeSys.HC.Z slot in place: rename it 409 + # to /HomeSys.HC and point at a smaller byte count, while 410 + # keeping the same cluster (so no data shifts). 411 + # - Leave slot 23 as zero — the kernel needs a terminator. 412 + # ---------------------------------------------------------------- 413 + root_off = root_clus * SECT 414 + 415 + # New /VodBrowser/ directory 416 + slot0 = root_off + used * DIR_ENTRY 417 + image[slot0 : slot0 + DIR_ENTRY] = _serialize_dir_entry( 418 + "VodBrowser", RS_ATTR_DIR, vod_dir_clus, vod_dir_size_field) 419 + 420 + # Rewritten /HomeSys.HC (was /HomeSys.HC.Z) 421 + image[homesys_off : homesys_off + DIR_ENTRY] = _serialize_dir_entry( 422 + "HomeSys.HC", RS_ATTR_FILE, old_homesys_clus, len(homesys_hc)) 423 + 424 + # Overwrite the HomeSys file data at its original cluster. Zero 425 + # out the rest of the allocation so stale .Z bytes don't leak 426 + # into the plain-text read. 427 + hs_data_off = old_homesys_clus * SECT 428 + image[hs_data_off : hs_data_off + homesys_cap] = ( 429 + homesys_hc + b"\x00" * (homesys_cap - len(homesys_hc))) 430 + 431 + # ---------------------------------------------------------------- 432 + # Update the headers that carry the ISO size. 433 + # ---------------------------------------------------------------- 434 + new_total_512 = len(image) // SECT 435 + new_total_rs = new_total_512 - BOOT_SECT_NUM 436 + new_total_cd = len(image) // CD_SECT 437 + 438 + # RedSea boot sector: total_sects at offset 16 439 + struct.pack_into("<Q", image, BOOT_SECT_NUM * SECT + 16, new_total_rs) 440 + 441 + # ISO9660 PVD: volume_space_size at offset 80 (both-endian u32) 442 + pvd_off = 16 * CD_SECT 443 + struct.pack_into("<I", image, pvd_off + 80, new_total_cd) 444 + struct.pack_into(">I", image, pvd_off + 84, new_total_cd) 445 + 446 + # Supplementary VD (CD sector 18) is a copy of the PVD with 447 + # type=0x02 — patch the same field there too so ISO9660 readers 448 + # that consult it see the right size. 449 + svd_off = 18 * CD_SECT 450 + struct.pack_into("<I", image, svd_off + 80, new_total_cd) 451 + struct.pack_into(">I", image, svd_off + 84, new_total_cd) 452 + 453 + # Allocation bitmap: the stock ISO only has the original 33792 454 + # sectors marked; we need to flip the bits for our new sectors 455 + # to "allocated" so the FS driver doesn't think they're free. 456 + # The bitmap starts at 512-byte sector 89 and is 9 sectors 457 + # (4608 bytes = 36864 bits) long. Bit N corresponds to 512-byte 458 + # sector N (counted from the start of the disc, not from 459 + # drv_offset). Flip all bits from start_sect..new_total_512-1. 460 + bitmap_off = (BOOT_SECT_NUM + 1) * SECT 461 + bitmap_bits = BITMAP_SECTS * SECT * 8 462 + if new_total_512 > bitmap_bits: 463 + raise ValueError( 464 + f"new ISO has {new_total_512} sectors but the 9-sector " 465 + f"bitmap only covers {bitmap_bits}. Need to grow the " 466 + f"bitmap or trim the VodBrowser file set.") 467 + for s in range(start_sect, new_total_512): 468 + byte_i = bitmap_off + (s // 8) 469 + bit_i = s & 7 470 + image[byte_i] |= 1 << bit_i 471 + 472 + with open(output_path, "wb") as f: 473 + f.write(image) 474 + 475 + print(f" stock ISO: {base_path}") 476 + print(f" stock size: {new_total_512 - (new_total_512 - start_sect)} sectors") 477 + print(f" new content: {new_total_512 - start_sect} sectors " 478 + f"(starting at sector {start_sect})") 479 + print(f" output size: {len(image)} bytes " 480 + f"({new_total_cd} CD sectors, {new_total_512} 512-byte sectors)") 481 + 482 + 483 + # ================================================================== 484 + # CLI 485 + # ================================================================== 486 + 487 + 488 + def main() -> int: 489 + parser = argparse.ArgumentParser(description=__doc__) 490 + parser.add_argument("--base", required=True, 491 + help="path to stock TempleOS.iso") 492 + parser.add_argument("--output", required=True, 493 + help="where to write the new bootable ISO") 494 + parser.add_argument("vodbrowser_files", nargs="+", 495 + help="host paths to VodBrowser .HC / .prg files; " 496 + "optional :name suffix overrides the in-ISO " 497 + "basename") 498 + args = parser.parse_args() 499 + 500 + # Sanity-check the base ISO: parse it, count entries, confirm the 501 + # root directory has enough slack for our two new entries. We 502 + # only need the reader here — inject_distro re-opens the file 503 + # fresh to do the actual patching. 504 + print(f"Reading base distro: {args.base}") 505 + _, root, _ = read_redsea(args.base) 506 + print(f" parsed {_count_files(root)} entries, root_clus={root.clus}") 507 + 508 + # Load every VodBrowser file into memory. Accept optional 509 + # ":name" overrides so caller can rename a file inside the ISO 510 + # without moving it on the host. 511 + vod_files: list[tuple[str, bytes]] = [] 512 + print("Collecting VodBrowser files:") 513 + for spec in args.vodbrowser_files: 514 + if ":" in spec: 515 + host_path, iso_name = spec.split(":", 1) 516 + else: 517 + host_path = spec 518 + iso_name = os.path.basename(host_path) 519 + with open(host_path, "rb") as f: 520 + data = f.read() 521 + vod_files.append((iso_name, data)) 522 + print(f" {iso_name:24s} {len(data):8d} bytes") 523 + 524 + # HomeSys.HC is the script that bootstraps the entire TempleOS 525 + # user session — it defines UserStartUp() / SrvStartUp(), spawns 526 + # the two User tasks, tiles the windows, prints the boot time, 527 + # and kicks the main user task into Once.HC (which is what 528 + # produces the welcome doc + "Install onto hard drive (y/n)?" 529 + # prompt). If we just replace HomeSys.HC with `#include 530 + # VodBrowser.HC;` the distro boots into a half-initialised 531 + # environment — no welcome doc, no install prompt, only one 532 + # user task, no window tiling. 533 + # 534 + # So we start from the canonical HomeSys.HC (taken verbatim from 535 + # the TempleOS V5.03 / TinkerOS source tree) and ADD three 536 + # XTalk lines at the end of StartUpTasks that feed additional 537 + # commands into the main user task's input queue: 538 + # 539 + # 1. `Cd(\"::/VodBrowser\");;#include \"VodBrowser.HC\";` — 540 + # compiles VodBrowser and all its dependencies. The `;;` 541 + # forces the Cd to execute at PARSE time so the compiler's 542 + # current directory is /VodBrowser when the lexer reaches 543 + # the #include. 544 + # 2. A colorised println telling the user to type `VodBrowser;` 545 + # to launch. 546 + # 547 + # Queue order: Once.HC runs first (welcome + install prompt), 548 + # and the moment it returns, the VodBrowser include runs, and 549 + # the hint prints. The user lands at the HolyC REPL with 550 + # everything loaded and a clear next step. If VodBrowser 551 + # crashes on launch, Esc just drops them back to the same REPL 552 + # — they don't get kicked into a boot loop. 553 + # Two XTalk lines per user task — the combined text goes into 554 + # each task's input queue and is executed line-by-line. user1 555 + # goes through the normal Once.HC install prompt first; user2 556 + # skips it (stock HomeSys.HC only sends Once to user1). After 557 + # Once.HC returns on user1, both tasks Cd + #include VodBrowser, 558 + # giving each REPL the full VodBrowser API. 559 + # 560 + # The "VodBrowser loaded. Type VodBrowser;" launch hint is NOT 561 + # XTalk'd — an earlier attempt XTalk'd a bare string literal as 562 + # a command, and the Tour doc (running between Once.HC and our 563 + # VodBrowser include) stole keystrokes from the same input queue, 564 + # corrupting the literal and tripping a HolyC lex error. Instead 565 + # the banner is printed from inside VodBrowser.HC itself (a 566 + # top-level statement that runs as a side effect of #include), 567 + # so both tasks see it right after their include finishes and 568 + # there's no fragile input-queue timing to get wrong. 569 + homesys_src = ( 570 + "//Place this file in /Home and change\n" 571 + "//anything you want. (VodBrowser distro)\n" 572 + "\n" 573 + "U0 UserStartUp()\n" 574 + "{//Run each time a user is spawned\n" 575 + " DocTermNew;\n" 576 + " Type(\"::/Doc/Start.DD\");\n" 577 + " LBts(&Fs->display_flags,DISPLAYf_SHOW);\n" 578 + " WinToTop;\n" 579 + " WinZBufUpdate;\n" 580 + " Dir;\n" 581 + "}\n" 582 + "\n" 583 + "U0 SrvStartUp()\n" 584 + "{//Run each time a srv task is spawned.\n" 585 + " DocTermNew;\n" 586 + " LBts(&Fs->display_flags,DISPLAYf_SHOW);\n" 587 + " WinToTop;\n" 588 + " WinZBufUpdate;\n" 589 + "}\n" 590 + "\n" 591 + "U0 StartUpTasks()\n" 592 + "{\n" 593 + " CTask *user1,*user2;\n" 594 + " user1=User;\n" 595 + " user2=User;\n" 596 + " WinToTop(user1);\n" 597 + " WinTileVert;\n" 598 + " \"Boot Time:%7.3fs\\n\",tS;\n" 599 + " XTalk(user1,\"Cd;#include \\\"Once\\\";\\n\");\n" 600 + " XTalk(user1,\"Cd(\\\"::/VodBrowser\\\");;#include \\\"VodBrowser.HC\\\";\\n\");\n" 601 + " XTalk(user2,\"Cd(\\\"::/VodBrowser\\\");;#include \\\"VodBrowser.HC\\\";\\n\");\n" 602 + "}\n" 603 + "\n" 604 + "StartUpTasks;\n" 605 + "\n" 606 + "\"\\nTempleOS V%5.3f\\t%D %T\\n\\n\"\n" 607 + ",sys_os_version,sys_compile_time,sys_compile_time;\n" 608 + ) 609 + homesys_hc = homesys_src.encode("ascii") 610 + print(f"\nHomeSys.HC {len(homesys_hc):8d} bytes (auto-launch)") 611 + 612 + print(f"\nWriting {args.output}...") 613 + inject_distro(args.base, vod_files, homesys_hc, args.output) 614 + return 0 615 + 616 + 617 + def _count_files(node: Node) -> int: 618 + n = 1 619 + for c in node.children: 620 + n += _count_files(c) if c.is_dir else 1 621 + return n 622 + 623 + 624 + if __name__ == "__main__": 625 + sys.exit(main())
+287
tools/mkredseaiso.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + mkredseaiso.py — Generate a RedSea ISO from scratch for TempleOS V5.03 4 + 5 + Layout (matching TempleOS distro ISO structure): 6 + CD sector 0-15: System area (zeros) 7 + CD sector 16: ISO9660 Primary Volume Descriptor 8 + CD sector 17: El Torito Boot Record Volume Descriptor 9 + CD sector 18: Supplementary Volume Descriptor 10 + CD sector 19: Volume Descriptor Set Terminator 11 + CD sector 20: El Torito Boot Catalog 12 + CD sector 21: Stage 1 boot loader (optional, from --bootcode) 13 + CD sector 22: RedSea boot sector (512-byte sector 88) 14 + Sectors 89-97: Allocation bitmap (9 sectors) 15 + Sector 98+: Root directory, then contiguous file data 16 + 17 + Usage: 18 + mkredseaiso.py --output dev.iso [--bootcode boot.bin] file1.HC file2.HC ... 19 + mkredseaiso.py --output dev.iso --extract-bootcode TOS_Distro.ISO 20 + """ 21 + import argparse 22 + import struct 23 + import os 24 + import sys 25 + 26 + SECT = 512 27 + CD_SECT = 2048 28 + DIR_ENTRY = 64 29 + BOOT_SECT_NUM = 88 # 512-byte sector number of RedSea boot sector 30 + BITMAP_SECTS = 9 31 + # Root dir must start at a CD-sector boundary (multiple of 4 x 512-byte sectors) 32 + # Boot(1) + bitmap(9) = 10 sectors from 88 = sector 98. Next CD boundary = 100. 33 + ROOT_CLUS = 100 # CD-sector aligned (sector 100 = CD sector 25) 34 + 35 + RS_ATTR_DIR = 0x0810 36 + RS_ATTR_FILE = 0x0C00 37 + 38 + 39 + def w16(v): 40 + return struct.pack('<H', v) 41 + 42 + def w32(v): 43 + return struct.pack('<I', v) 44 + 45 + def w64(v): 46 + return struct.pack('<Q', v) 47 + 48 + def w32_both(v): 49 + """ISO9660 both-endian 32-bit""" 50 + return struct.pack('<I', v) + struct.pack('>I', v) 51 + 52 + def w16_both(v): 53 + """ISO9660 both-endian 16-bit""" 54 + return struct.pack('<H', v) + struct.pack('>H', v) 55 + 56 + def pad(data, size): 57 + return data + b'\x00' * (size - len(data)) 58 + 59 + 60 + def make_pvd(volume_size_cd_sects): 61 + """ISO9660 Primary Volume Descriptor (CD sector 16)""" 62 + pvd = bytearray(CD_SECT) 63 + pvd[0] = 0x01 # type 64 + pvd[1:6] = b'CD001' # identifier 65 + pvd[6] = 0x01 # version 66 + # volume space size at offset 80 (both-endian) 67 + pvd[80:88] = w32_both(volume_size_cd_sects) 68 + # volume set size at offset 120 (both-endian) 69 + pvd[120:124] = w16_both(1) 70 + # volume sequence number at offset 124 (both-endian) 71 + pvd[124:128] = w16_both(1) 72 + # logical block size at offset 128 (both-endian) 73 + pvd[128:132] = w16_both(CD_SECT) 74 + # root_clus as both-endian u32 at offset 152 (TempleOS reads this!) 75 + pvd[152:160] = w32_both(ROOT_CLUS) 76 + # file_structure_version at offset 881 77 + pvd[881] = 0x01 78 + # "TempleOS RedSea" at offset 314 (TempleOS publisher_id — detection key!) 79 + pvd[314:314+15] = b'TempleOS RedSea' 80 + return bytes(pvd) 81 + 82 + 83 + def make_boot_record(): 84 + """El Torito Boot Record Volume Descriptor (CD sector 17)""" 85 + br = bytearray(CD_SECT) 86 + br[0] = 0x00 # type: boot record 87 + br[1:6] = b'CD001' 88 + br[6] = 0x01 # version 89 + br[7:30] = pad(b'EL TORITO SPECIFICATION', 23) 90 + # boot catalog location at offset 0x47 (little-endian u32) 91 + struct.pack_into('<I', br, 0x47, 20) # CD sector 20 92 + return bytes(br) 93 + 94 + 95 + def make_supplementary_vd(volume_size_cd_sects): 96 + """Supplementary Volume Descriptor (CD sector 18) — copy of PVD with different type""" 97 + svd = bytearray(make_pvd(volume_size_cd_sects)) 98 + svd[0] = 0x02 # type: supplementary 99 + return bytes(svd) 100 + 101 + 102 + def make_terminator(): 103 + """Volume Descriptor Set Terminator (CD sector 19)""" 104 + t = bytearray(CD_SECT) 105 + t[0] = 0xFF 106 + t[1:6] = b'CD001' 107 + t[6] = 0x01 108 + return bytes(t) 109 + 110 + 111 + def make_boot_catalog(): 112 + """El Torito Boot Catalog (CD sector 20)""" 113 + cat = bytearray(CD_SECT) 114 + 115 + # Validation Entry (32 bytes) 116 + val = bytearray(32) 117 + val[0] = 0x01 # header ID 118 + val[1] = 0x00 # platform: x86 119 + val[4:12] = pad(b'TempleOS', 8) # ID string 120 + # Checksum at bytes 28-29: sum of all 16-bit words must be 0 121 + val[30] = 0x55 # key byte 1 122 + val[31] = 0xAA # key byte 2 123 + # Calculate checksum 124 + total = 0 125 + for i in range(0, 32, 2): 126 + total += struct.unpack_from('<H', val, i)[0] 127 + checksum = (0x10000 - (total & 0xFFFF)) & 0xFFFF 128 + struct.pack_into('<H', val, 28, checksum) 129 + cat[0:32] = val 130 + 131 + # Default Boot Entry (32 bytes) 132 + boot = bytearray(32) 133 + boot[0] = 0x88 # bootable 134 + boot[1] = 0x00 # no emulation 135 + struct.pack_into('<H', boot, 2, 0) # load segment (default) 136 + boot[4] = 0x00 # system type 137 + struct.pack_into('<H', boot, 6, 4) # sector count (4 * 512 = 2048) 138 + struct.pack_into('<I', boot, 8, 21) # load RBA: CD sector 21 139 + cat[32:64] = boot 140 + 141 + return bytes(cat) 142 + 143 + 144 + def make_redsea_boot(drv_offset, total_sects, root_clus, bitmap_sects, unique_id=0x1234): 145 + """RedSea boot sector (512 bytes)""" 146 + bs = bytearray(SECT) 147 + bs[3] = 0x88 # MBR_PT_REDSEA signature 148 + struct.pack_into('<Q', bs, 8, drv_offset) 149 + struct.pack_into('<Q', bs, 16, total_sects) 150 + struct.pack_into('<Q', bs, 24, root_clus) 151 + struct.pack_into('<Q', bs, 32, bitmap_sects) 152 + struct.pack_into('<Q', bs, 40, unique_id) 153 + struct.pack_into('<H', bs, 510, 0xAA55) 154 + return bytes(bs) 155 + 156 + 157 + def make_dir_entry(attr, name, clus, size): 158 + """64-byte RedSea directory entry""" 159 + e = bytearray(DIR_ENTRY) 160 + struct.pack_into('<H', e, 0, attr) 161 + name_bytes = name.encode('ascii')[:37] 162 + e[2:2+len(name_bytes)] = name_bytes 163 + struct.pack_into('<Q', e, 40, clus) 164 + struct.pack_into('<Q', e, 48, size) 165 + # datetime left as zeros 166 + return bytes(e) 167 + 168 + 169 + def main(): 170 + parser = argparse.ArgumentParser() 171 + parser.add_argument('--output', required=True) 172 + parser.add_argument('--bootcode', help='Stage 1 boot loader binary (2048 bytes)') 173 + parser.add_argument('--extract-bootcode', help='Extract boot code from a TempleOS ISO') 174 + parser.add_argument('files', nargs='*') 175 + args = parser.parse_args() 176 + 177 + # Extract boot code mode 178 + if args.extract_bootcode: 179 + with open(args.extract_bootcode, 'rb') as f: 180 + iso = f.read() 181 + bootcode = iso[21 * CD_SECT : 22 * CD_SECT] 182 + outpath = args.output 183 + with open(outpath, 'wb') as f: 184 + f.write(bootcode) 185 + print(f'Extracted {len(bootcode)} bytes of boot code to {outpath}') 186 + return 187 + 188 + if not args.files: 189 + parser.error('No input files specified') 190 + 191 + # Load boot code if provided 192 + bootcode = b'\x00' * CD_SECT 193 + if args.bootcode and os.path.exists(args.bootcode): 194 + with open(args.bootcode, 'rb') as f: 195 + bootcode = f.read() 196 + bootcode = (bootcode + b'\x00' * CD_SECT)[:CD_SECT] 197 + 198 + # Gather files 199 + files = [] 200 + for path in args.files: 201 + name = os.path.basename(path) 202 + with open(path, 'rb') as f: 203 + data = f.read() 204 + files.append((name, data)) 205 + 206 + # Layout 207 + # dir_bytes must be sector-aligned. 208 + # Minimum 3 sectors (matching real TempleOS ISO). 209 + # File data must start on a CD sector boundary (4 x 512-byte sectors). 210 + num_entries = 2 + len(files) 211 + dir_sects = (num_entries * DIR_ENTRY + SECT - 1) // SECT 212 + if dir_sects < 3: 213 + dir_sects = 3 214 + dir_bytes = dir_sects * SECT 215 + 216 + # Align data_start to CD sector boundary (multiple of 4 x 512-byte sectors) 217 + data_start = ROOT_CLUS + dir_sects 218 + data_start = ((data_start + 3) // 4) * 4 219 + cur = data_start 220 + file_sectors = [] 221 + for name, data in files: 222 + file_sectors.append(cur) 223 + sects = (len(data) + SECT - 1) // SECT 224 + if sects == 0: 225 + sects = 1 226 + cur += sects 227 + 228 + # Total CD sectors must be enough for all data. 229 + # Minimum 8470 CD sectors (17MB) — matches real TempleOS ISO size. 230 + # Smaller images cause issues with the ATAPI driver's buffering. 231 + total_cd_sects = (cur * SECT + CD_SECT - 1) // CD_SECT 232 + if total_cd_sects < 8470: 233 + total_cd_sects = 8470 234 + iso_size = total_cd_sects * CD_SECT 235 + 236 + # sects MUST equal total_512_sectors - drv_offset 237 + # TempleOS validates this against the ATAPI-reported disc size 238 + total_512_sects = iso_size // SECT 239 + total_rs_sects = total_512_sects - BOOT_SECT_NUM 240 + 241 + # Build image 242 + img = bytearray(iso_size) 243 + 244 + # CD sectors 16-19: ISO9660 volume descriptors 245 + img[16*CD_SECT : 17*CD_SECT] = make_pvd(total_cd_sects) 246 + img[17*CD_SECT : 18*CD_SECT] = make_boot_record() 247 + img[18*CD_SECT : 19*CD_SECT] = make_supplementary_vd(total_cd_sects) 248 + img[19*CD_SECT : 20*CD_SECT] = make_terminator() 249 + 250 + # CD sector 20: El Torito boot catalog 251 + img[20*CD_SECT : 21*CD_SECT] = make_boot_catalog() 252 + 253 + # CD sector 21: boot code 254 + img[21*CD_SECT : 22*CD_SECT] = bootcode[:CD_SECT] 255 + 256 + # RedSea boot sector at 512-byte sector 88 257 + bs = make_redsea_boot(BOOT_SECT_NUM, total_rs_sects, ROOT_CLUS, BITMAP_SECTS) 258 + img[BOOT_SECT_NUM*SECT : BOOT_SECT_NUM*SECT + SECT] = bs 259 + 260 + # Bitmap: all 0xFF (everything allocated) 261 + for i in range(BITMAP_SECTS): 262 + off = (BOOT_SECT_NUM + 1 + i) * SECT 263 + img[off:off+SECT] = b'\xFF' * SECT 264 + 265 + # Root directory 266 + root_off = ROOT_CLUS * SECT 267 + img[root_off:root_off+DIR_ENTRY] = make_dir_entry(RS_ATTR_DIR, '.', ROOT_CLUS, dir_bytes) 268 + img[root_off+DIR_ENTRY:root_off+2*DIR_ENTRY] = make_dir_entry(RS_ATTR_DIR, '..', ROOT_CLUS, 0) 269 + 270 + for i, (name, data) in enumerate(files): 271 + entry_off = root_off + (i + 2) * DIR_ENTRY 272 + img[entry_off:entry_off+DIR_ENTRY] = make_dir_entry(RS_ATTR_FILE, name, file_sectors[i], len(data)) 273 + 274 + # File data 275 + foff = file_sectors[i] * SECT 276 + img[foff:foff+len(data)] = data 277 + 278 + with open(args.output, 'wb') as f: 279 + f.write(img) 280 + 281 + print(f'Created {args.output} ({len(img)} bytes, {total_cd_sects} CD sectors, {len(files)} files)') 282 + for i, (name, data) in enumerate(files): 283 + print(f' {name:20s} {len(data):6d} bytes @ sector {file_sectors[i]}') 284 + 285 + 286 + if __name__ == '__main__': 287 + main()