#! /usr/bin/env python3 # Copyright (c) 2025 Blashyrkh # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # WARNING: the code is somewhat messy. Don't try to fully understand it, and if # you do anyway then do it on your own risk. PC: int = 0 MEMORY: list[int] = [] LABELS: dict[str, int] = {} def store1(address: int, value: int): assert 0<=value<0x100 assert 0<=address<0x1000000 global MEMORY while address>=len(MEMORY): MEMORY.append(0) MEMORY[address] = value def store2(address: int, value: int): assert 0<=value<0x10000 assert 0<=address<0x1000000-1 global MEMORY while address+1>=len(MEMORY): MEMORY.append(0) MEMORY[address] = value>>8 MEMORY[address+1] = value&0xFF def store3(address: int, value: int): assert 0<=value<0x1000000 assert 0<=address<0x1000000-2 global MEMORY while address+2>=len(MEMORY): MEMORY.append(0) MEMORY[address] = value>>16 MEMORY[address+1] = (value>>8)&0xFF MEMORY[address+2] = value&0xFF def init_entry_point(address: int): store3(2, address) def init_video_bank(bankno: int): store1(5, bankno) def init_audio_page(pageno: int): store2(6, pageno) def org(offset: int): global PC PC = offset def db(value: int): global PC store1(PC, value) PC += 1 def movj(source: int, destination: int, jump_point: int): global PC store3(PC, source) store3(PC+3, destination) store3(PC+6, jump_point) PC += 9 def jump(jump_point: int): movj(0, 0, jump_point) def mov(source: int, destination: int): movj(source, destination, PC+9) def nop(): movj(0, 0, PC+9) def hlt(): movj(0, 0, PC) def setlabel(label: str, where: int = None): global LABELS, PC LABELS[label] = PC if where is None else where def getlabel(label: str) -> int: global LABELS, PC return LABELS.get(label, 0) def offsetof(label: str) -> int: global LABELS, PC return LABELS.get(label, 0)-PC def save(filename: str): global MEMORY s = len(MEMORY) while s>0 and MEMORY[s-1]==0: s -= 1 with open(filename, "wb") as f: f.write(bytes(MEMORY[:s])) def program(pass_no: int): # Memory map: # # 0000xx: |HDR|VB|HLT|CHK|......| BT| # 0001xx: | SHADER |.....| BT| # 0002xx: | RELOC |........|BT| # 0003xx: |PLT|...................|AVG| # 0004xx: | DECTBL | # 0005xx: | RNDMAP | ____ initialized data ends here # 0006xx: | ISZERO | # 00FFxx: | AUDIO | # 01xxxx: | SUBTBL | # 02xxxx: | VIDEO0 | # 03xxxx: | VIDEO1 | # YYXXxx: |SHADER INSTANCE| | # # Legend: # ... - space for code allocation. # HDR - 8 bytes header (special mapped registers) # VB - current offscreen video bank (02 or 03). Opposite to the visible one. Used by the shader. It's essential # to use double-buffered drawing because we can't produce the entire picture in a single frame span. # HLT - hlt instruction. # CHK - checkpoints (and entrypoint). All checkpoints are in page 0000, so it's possible to set a checkpoint by # changing single byte at address 0x000004 # BT - branching table. Each entry is the first instruction (with explicit following jump) of particular branch # of execution. Page 0000 contains non-zero branch, page 0001 contains zero branch. Page 0002 is a special # case of 3-way branching (used in shader relocation) # SHADER - template program that processes single pixel. It's duplicated for each pixel and "relocated" - X and Y # are used to modify some bytes of the shader. Instances of the shader are chained together. # RELOC - relocation table. Each byte of RELOC corresponds to a byte of the SHADER. 0 means "no relocation needed", # 1 means "relocate by X", 2 means "relocate by Y". "Relocate" means "replace original value with X|Y minus # value". # PLT - palette. Mapping from index 0..14 to color (from black to white through red and yellow). # AVG - averaging table. Maps 0xFF-sum(four pixels) back to 0..14 range. # DECTBL - decrement table, also used as compile-time contant table (if we need to write immediate value somewhere). # RNDMAP - mapping from random value [0..255] to color index, 0 has special meaning "do not change" # ISZERO - runtime-initialized table. All values are zeroes except the first one which is set to 1. # AUDIO - we don't produce sound (maybe in next version) # SUBTBL - subtraction table which is generated in runtime using DECTBL and two nested loops # VIDEO0 - two banks of video memory. We can't build the whole picture in a single 65536-instructions frame, and we # VIDEO1 don't want to display unpleasant effects. So we draw fire on offscreen plane, and then switch planes. # Fire area X_MIN = 10 X_MAX = 245 Y_MIN = 207 Y_MAX = 254 ROWS_PER_FRAME = 25 # TODO: calculate from SHADER_SIZE, X_MAX and X_MIN SHADER_INST0 = (Y_MIN+0*ROWS_PER_FRAME)*65536+X_MIN*256+0 SHADER_INST1 = (Y_MIN+1*ROWS_PER_FRAME)*65536+X_MIN*256+0 # My signature in the first two bytes. Doesn't affect anything because it's rewritten with keyboard state. org(0x000000) db(0x52) db(0x53) init_entry_point(getlabel("ENTRY")) init_video_bank(0x02) init_audio_page(0x00FF) org(8) setlabel("VB") db(0x03) setlabel("HLT") hlt() org(0x000100) setlabel("SHADER") movj(0x01FF00, 0x000012+1, 0x000009) # YYXX00 movj(0xFF0000+2, 0x000012+2, 0x000012) # YYXX09 movj(0x010000, 0x000024+1, 0x00001B) # YYXX12 movj(0xFFFF00+2, 0x000024+2, 0x000024) # YYXX1B movj(0x010000, 0x000036+1, 0x00002D) # YYXX24 movj(0xFF0100+2, 0x000036+2, 0x000036) # YYXX2D movj(0x010000, 0x00003F+2, 0x00003F) # YYXX36 movj(getlabel("AVG"), 0x010000+2, 0x000048) # YYXX3F movj(0x010000+2, 0x00005A+2, 0x000051) # YYXX48 movj(getlabel("VB"), 0x00005A+3, 0x00005A) # YYXX51 movj(getlabel("PALETTE"), 0x03FF00, 0x00FF00) # YYXX5A SHADER_SIZE = -offsetof("SHADER") org(0x000200) setlabel("RELOC") reloc = [0, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, 2, 1, 0, 0, 0, 0, 0, 2, 1, 2, 1, 0] for v in reloc: db(v) # Both palette and averaging table are here. Cells [0, 14] contain palette (from black to white). # Cells [199, 255] contain averaging table: mapping of 255 minus sum of 4 cells to one cell, # back to range [0, 14]. # Averaging introduces fading of darker shades. # Cell range [15, 198] is available for code allocation org(0x000300) setlabel("PALETTE") setlabel("AVG") palette = [0x00, 0x24, 0x48, 0x6C, 0x90, 0xB4, 0xBA, 0xC0, 0xC6, 0xCC, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x0E, 0x0E, 0x0D, 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0B, 0x0B, 0x0B, 0x0B, 0x0A, 0x0A, 0x0A, 0x0A, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, 0x07, 0x06, 0x06, 0x06, 0x06, 0x05, 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x03, 0x03, 0x03, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] for v in palette: db(v) # Decrement table. Used for simple countdown loops and to fill subtraction table org(0x000400) setlabel("DECTBL") for i in range(256): db((i+255)&0xFF) # Map from random number to palette index or zero (which means "no change"). "No change" case makes fire # less chaotic org(0x000500) setlabel("RNDMAP") rndmap = [0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] for v in rndmap: db(v) # IsZero table (with only one "1" value which is written during startup). It's used to implement branching: # copy value from this table to PC+7, and you should jump either to xx00xx or to xx01xx depending on the # value being tested. org(0x000600) setlabel("ISZERO") # I use decrement table as constant table too (it's known at compile time where each constant resides). def constant(N): assert 0<=N<256 return getlabel("DECTBL")+(N+1)%256 # Branching table - first instruction (sometimes it's just a jump) of particular branch. Page0 contains # non-zero branch. I.e. every conditional jump organized with ISZERO or RELOC leads here when condition # is "non-zero". Page1 contains zero branch # It contains the following branchings: # - 000?F7 - relocation # - 000?EE - inner cycle of building of subtraction table # - 000?E5 - outer cycle of building of subtraction table # - 000?DC - inner cycle of copying of shader code (copy+relocate) # - 000?D3 - middle cycle of copying of shader code (row filled) # - 000?CA - outer cycle of copying of shader code (everything is done) # - 000?C1 - random fill no-change flag # - 000?B8 - random fill loop org(0x0000F7) setlabel("X", PC+4) setlabel("Y", PC+3) movj(getlabel("TMP"), Y_MIN*65536+X_MIN*256, getlabel("L7")) # 0000F7 org(0x0001F7) movj(getlabel("X"), getlabel("L6")+9+1, getlabel("L6")) # 0001F7 org(0x0002F7) movj(getlabel("Y"), getlabel("L6")+9+1, getlabel("L6")) # 0002F7 org(0x0000EE) setlabel("A", PC+5) setlabel("B", PC+4) setlabel("B-A", PC+2) movj(constant(0), 0x01FFFF, getlabel("L1")) # 0000EE org(0x0001EE) jump(getlabel("L2")) # 0001EE org(0x0000E5); jump( 0x0000EE) # 0000E5 org(0x0001E5); movj(constant(getlabel("CHK1")), 4, getlabel("HLT")) # 0001E5 org(0x0000DC); movj(getlabel("K"), getlabel("L4")+2, getlabel("L4")) # 0000DC org(0x0001DC); movj(getlabel("X"), getlabel("L8")+1, getlabel("L8")) # 0001DC org(0x0000D3); movj(getlabel("X+1"), getlabel("X"), getlabel("CHK1")) # 0000D3 org(0x0001D3); movj(getlabel("Y"), getlabel("L10")+1, getlabel("L10")) # 0001D3 org(0x0000CA); movj(getlabel("Y+1"), getlabel("Y"), getlabel("CHK1")) # 0000CA org(0x0001CA); movj(constant(0), (Y_MIN+ROWS_PER_FRAME-1)*65536+X_MAX*256+0x5A+6, getlabel("L11")) org(0x0000C1); movj(getlabel("XX"), getlabel("L13")+4, getlabel("L13")) # 0000C1 org(0x0001C1); setlabel("XX", PC+1) movj(0x0100FF, getlabel("XX"), getlabel("L14")) # 0001C1 org(0x0000B8); jump(getlabel("L12")) org(0x0001B8); movj(getlabel("VB"), getlabel("TMP"), getlabel("L15")) org(0x000012) # Now the program starts setlabel("ENTRY") movj(constant(1), getlabel("ISZERO"), 0x0000EE) # 000012 setlabel("CHK1") movj(constant(SHADER_SIZE), getlabel("K"), getlabel("L3")) # 00001B setlabel("CHK2") movj(constant(getlabel("CHK3")), 0x000004, SHADER_INST0) # 000024 setlabel("CHK3") movj(constant(getlabel("CHK2")), 0x000004, SHADER_INST1) # 00002D # Fill subtraction table in bank01. 0x[01][B][A] should contain (B-A) mod 256. # The first instruction of loop body is at 0x0000EE setlabel("L1") mov(getlabel("B-A"), PC+9+2) # 000036 mov(getlabel("DECTBL"), getlabel("B-A")) # 00003F mov(getlabel("B"), PC+9+2) # 000048 mov(getlabel("DECTBL"), getlabel("B")) # 000051 mov(PC-9+2, PC+9+2) # 00005A movj(getlabel("ISZERO"), PC+7, 0x0000EE) # 000063 setlabel("L2") mov(getlabel("L1")+9+2, getlabel("B-A")) # 00006C mov(getlabel("A"), PC+9+2) # 000075 mov(getlabel("DECTBL"), getlabel("A")) # 00007E mov(PC-9+2, PC+9+2) # 000087 movj(getlabel("ISZERO"), PC+7, 0x0000E5) # 000090 # Copy SHADER and relocate it (fix addresses) setlabel("L3") setlabel("K", PC+2) movj(getlabel("ISZERO"), PC+7, 0x0000DC) # 000099 setlabel("L4") mov(getlabel("DECTBL"), getlabel("K-1")) # 0000A2 setlabel("K-1", PC+2) movj(getlabel("SHADER"), getlabel("TMP"), getlabel("L5")) # 0000AB org(0x000100+SHADER_SIZE) setlabel("L5") mov(getlabel("K-1"), 0x0000F7+5) # 000163 mov(getlabel("K-1"), PC+9+2) # 00016C movj(getlabel("RELOC"), PC+7, 0x0000F7) # 000175 setlabel("L6") mov(getlabel("TMP"), getlabel("L6")+9+2) # 00017E movj(0x010000, getlabel("TMP"), 0x0000F7) # 000187 setlabel("L7") movj(getlabel("K-1"), getlabel("K"), getlabel("L3")) # 000190 setlabel("L8") mov(0x0100FF, getlabel("X+1")) # 000199 setlabel("X+1", PC+1) mov(0x010000+(X_MAX+1), PC+9+2) # 0001A2 movj(getlabel("ISZERO"), PC+7, 0x0000D3) # 0001AB org(0x000200+SHADER_SIZE) setlabel("L10") mov(0x0100FF, getlabel("Y+1")) # 000263 mov(getlabel("Y"), PC+18+3) # 00026C mov(getlabel("Y"), PC+18+3) # 000275 mov(getlabel("Y+1"), 0*65536+X_MAX*256+0x5A+6) # 00027E mov(constant(X_MIN), 0*65536+X_MAX*256+0x5A+7) # 000287 mov(constant(X_MIN), getlabel("X")) # 000290 setlabel("Y+1", PC+1) mov(0x010000+(Y_MAX+1), PC+9+2) # 000299 movj(getlabel("ISZERO"), PC+7, 0x0000CA) # 0002A2 setlabel("L11") mov(constant(0), (Y_MIN+ROWS_PER_FRAME-1)*65536+X_MAX*256+0x5A+7) # 0002AB mov(constant(0x09), (Y_MIN+ROWS_PER_FRAME-1)*65536+X_MAX*256+0x5A+8)# 0002C6 mov(constant(0x00), Y_MAX*65536+X_MAX*256+0x5A+6) # 0002CD mov(constant(0x03), Y_MAX*65536+X_MAX*256+0x5A+7) # 0002D8 mov(constant(0x0F), Y_MAX*65536+X_MAX*256+0x5A+8) # 0002E1 movj(constant(getlabel("CHK3")), 0x000004, getlabel("HLT")) # 0002EA org(0x00030F) mov(constant(X_MIN), getlabel("XX")) # 00030F setlabel("L12") setlabel("PRNG[2]", PC+1) setlabel("PRNG[3]", PC+2) mov(0x011C91, getlabel("R")) # 000321 mov(getlabel("PRNG[2]"), getlabel("PRNG[3]")) # 00032A mov(getlabel("R"), PC+9+2) # 000333 mov(getlabel("ISZERO"), PC+9+2) # 00033C setlabel("PRNG[1]", PC+1) mov(0x01BC00, getlabel("PRNG[2]")) # 000345 setlabel("PRNG[0]", PC+1) mov(0x01E700, getlabel("PRNG[1]")) # 00034E mov(getlabel("R"), getlabel("PRNG[0]")) # 000357 setlabel("R", PC+2) mov(getlabel("RNDMAP"), getlabel("RI")) # 000360 setlabel("RI", PC+2) movj(getlabel("ISZERO"), PC+7, 0x0000C1) # 000369 setlabel("L13") mov(getlabel("RI"), Y_MAX*65536+2) # 000372 mov(getlabel("XX"), PC+9+4) # 00037B movj(getlabel("RI"), (Y_MAX+1)*65536+2, 0x0001C1) # 000384 setlabel("L14") mov(getlabel("XX"), PC+9+1) # 00038D mov(0x010000+(X_MAX+1), PC+9+2) # 000396 movj(getlabel("ISZERO"), PC+7, 0x0000B8) # 00039F setlabel("L15") mov(0x000005, getlabel("VB")) # 0003A8 movj(getlabel("TMP"), 0x000005, getlabel("HLT")) # 0003B1 if __name__=="__main__": program(pass_no=1) program(pass_no=2) save("fire.BytePusher")