Merge pull request #2 from KrisKennaway/dhgr

Dhgr
This commit is contained in:
KrisKennaway 2019-07-11 23:56:51 +01:00 committed by GitHub
commit 326ca62075
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3215 additions and 881 deletions

View File

@ -58,6 +58,7 @@ zpdummy = $08
dummy = $ffff
ptr = $06 ; TODO: we only use this for connection retry count
HGRZP = $E6 ; ZP location used by HGR internals to track page to clear
; soft-switches
KBD = $C000
@ -74,7 +75,7 @@ DHIRESON = $C05E
; MONITOR SUBROUTINES
HGR = $F3E2
HGR0 = $F3EA ; internal entry point within HGR that doesn't set soft-switches
HGR0 = $F3F2 ; internal entry point within HGR that doesn't set soft-switches
COUT = $FDED
PRBYTE = $FDDA
PRNTAX = $F941
@ -311,30 +312,35 @@ op_header:
; Initialize (D)HGR in the CODE segment so we don't accidentally toast ourselves when
; erasing HGR
_op_header_hgr:
; Co-opt HGR internals to clear screen without displaying it.
; nukes the startup code we placed in HGR segment
STA HIRESON
STA FULLSCR
LDA #$20
STA HGRZP ; ZP location used by HGR to track page to clear
JSR HGR0
LDA WDATA ; Video mode
BEQ @1 ; 0 = HGR mode
; TODO: clear screen before displaying it to look cleaner
; DHGR mode
STA TEXTOFF
STA HIRESON
STA DHIRESON
STA COL80ON
STA STORE80ON
; Clear aux screen
STA PAGE2ON ; AUX memory active
; Co-opt HGR internals to clear AUX for us.
LDA #$20
STA HGRZP
JSR HGR0
STA PAGE2OFF ; MAIN memory active
STA TEXTOFF ; now display empty (D)HGR screen. Doing this before the next instruction to make sure we don't see 80-column text garbage momentarily.
STA COL80ON
STA DHIRESON
@1:
JSR HGR ; nukes the startup code we placed in HGR segment
STA FULLSCR
STA TEXTOFF ; now display empty HGR screen (NOP for DHGR since we've already done it)
; establish invariants expected by decode loop
LDY #>RXBASE ; High byte of socket 0 receive buffer
@ -391,7 +397,7 @@ _op_header_hgr:
; Y register has the high byte of the W5100 address pointer in the RX socket code, so we
; can't trash this until we are ready to point back there.
checkrecv:
BIT TICK ; 4
STA TICK ; 4
LDA #<S0RXRSR ; 2 Socket 0 Received Size register
STA WADRL ; 4
@ -434,7 +440,7 @@ recv: ; 15 cycles so far
; X will usually already be 0 from op_ack except during first frame when reading
; header but reset it unconditionally
LDX #$00 ; 2
BIT TICK ; 4 ; 36
STA TICK ; 4 ; 36
NOP ; 2
STA dummy ; 4
@ -458,7 +464,7 @@ op_nop:
; - read 2 bytes from the stream as address of next opcode
;
; Each opcode has 6 cycles of padding, which is necessary to support reordering things to
; get the second "BIT TICK" at the right cycle offset.
; get the second "STA TICK" at the right cycle offset.
;
; Where possible we share code by JMPing to a common tail instruction sequence in one of the
; earlier opcodes. This is critical for reducing code size enough to fit.
@ -474,8 +480,8 @@ op_nop:
.macro op_tick_4 page
;4+(4)+2+4+4+4+5+4+5+4+5+4+5+4+4+4+4+3=73
.ident (.concat ("op_tick_4_page_", .string(page))):
BIT TICK ; 4
BIT TICK ; 4
STA TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
STA zpdummy ; 3
@ -518,9 +524,9 @@ tickident page, 7
.macro op_tick_6 page
;4+(2+4)+3+4+4+5+4+5+4+5+4+5+4+4+4+5+3
.ident (.concat ("op_tick_6_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
@ -563,9 +569,9 @@ tickident page, 8
.macro op_tick_8 page
;4+(4+4)+3+3+55
.ident (.concat ("op_tick_8_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_55")) ; 3 + 55
@ -574,10 +580,10 @@ tickident page, 8
.macro op_tick_10 page
;4+(4+2+4)+3+56
.ident (.concat ("op_tick_10_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_56")) ; 3 + 56
.endmacro
@ -585,10 +591,10 @@ tickident page, 8
.macro op_tick_12 page
;4+(4+4+4)+3+3+51
.ident (.concat ("op_tick_12_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_51")) ; 3 + 51
@ -597,11 +603,11 @@ tickident page, 8
.macro op_tick_14 page
;4+(4+4+2+4)+3+52
.ident (.concat ("op_tick_14_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_52")) ; 3+52
.endmacro
@ -609,14 +615,14 @@ tickident page, 8
.macro op_tick_16 page
; 4+(4+4+4+4)+5+2+3+43
.ident (.concat ("op_tick_16_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
; This temporarily violates X=0 invariant required by tick_6, but lets us share a
; common opcode tail; otherwise we need a dummy 4-cycle opcode between the ticks, which
; doesn't leave enough to JMP with.
LDX WDATA ; 4
LDY WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA page << 8,x ; 5
LDX #$00 ; 2 restore X=0 invariant
@ -627,14 +633,14 @@ tickident page, 8
.macro op_tick_18 page
; 4 + (4+4+4+2+4)+5+5+2+2+4+5+4+5+4+4+4+4+3
.ident (.concat ("op_tick_18_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
; lets us reorder the 5-cycle STA page << 8,y outside of tick loop.
; This temporarily violates X=0 invariant required by tick_6
LDX WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
STA page << 8,Y ; 5
STA page << 8,X ; 5
@ -661,12 +667,12 @@ tickident page, 8
.macro op_tick_20 page
;4+(4+4+5+3+4)+3+46=73
.ident (.concat ("op_tick_20_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_46"))
.endmacro
@ -675,12 +681,12 @@ tickident page, 8
.macro op_tick_22 page
; 4+(4+4+5+4+4)+3+3+42
.ident (.concat ("op_tick_22_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_42")) ; 3 + 42
@ -689,13 +695,13 @@ tickident page, 8
.macro op_tick_24 page
;4+(4+4+5+4+3+4)+3+42
.ident (.concat ("op_tick_24_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_42"))
.endmacro
@ -703,13 +709,13 @@ tickident page, 8
.macro op_tick_26 page ; pattern repeats from op_tick_8
; 4+(4+4+5+4+5+4)+3+37
.ident (.concat ("op_tick_26_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
STA page << 8,Y ; 5
BIT TICK; 4
STA TICK; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_37")) ; 3 + 37
@ -718,14 +724,14 @@ tickident page, 8
.macro op_tick_28 page ; pattern repeats from op_tick_10
; 4+(4+2+4+5+4+5+4)+3+38
.ident (.concat ("op_tick_28_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
STA page << 8,Y ; 5
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_38"))
.endmacro
@ -733,14 +739,14 @@ tickident page, 8
.macro op_tick_30 page ; pattern repeats from op_tick_12
;4+(4+4+5+4+5+4+4)+3+3+33
.ident (.concat ("op_tick_30_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_33")) ; 3 + 33
@ -749,7 +755,7 @@ tickident page, 8
.macro op_tick_32 page ; pattern repeats from op_tick_14
;4+(4+4+5+4+5+4+2+4)+3+34
.ident (.concat ("op_tick_32_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -757,7 +763,7 @@ tickident page, 8
STA page << 8,Y ; 5
LDY WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_34"))
.endmacro
@ -765,7 +771,7 @@ tickident page, 8
.macro op_tick_34 page ; pattern repeats from op_tick_16
; 4+(4+4+5+4+5+4+4+4)+2+5+5+3+20
.ident (.concat ("op_tick_34_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -773,7 +779,7 @@ tickident page, 8
STA page << 8,Y ; 5
LDY WDATA ; 4
LDX WDATA ; 4 ; allows reordering STA ...,X outside ticks
BIT TICK ; 4
STA TICK ; 4
STA page << 8,Y ; 5
STA page << 8,X ; 5
@ -786,7 +792,7 @@ tickident page, 8
.macro op_tick_36 page ; pattern repeats from op_tick_18
;4+(4+4+5+4+5+4+4+2+4)+5+5+2+2+4+4+4+4+3
.ident (.concat ("op_tick_36_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -795,7 +801,7 @@ tickident page, 8
LDY WDATA ; 4
LDX WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
STA page << 8,Y ; 5
STA page << 8,X ; 5
@ -814,7 +820,7 @@ tickident page, 8
.macro op_tick_38 page ; pattern repeats from op_tick_20
; 4 + (4+4+5+4+5+4+5+3+4)+3+28
.ident (.concat ("op_tick_38_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -823,7 +829,7 @@ tickident page, 8
LDY WDATA ; 4
STA page << 8,Y ; 5
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_28")) ; 3 + 28
.endmacro
@ -832,7 +838,7 @@ tickident page, 8
.macro op_tick_40 page ; pattern repeats from op_tick_22
;4+(4+4+5+4+5+4+5+4+4)+3+3+24
.ident (.concat ("op_tick_40_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -841,7 +847,7 @@ tickident page, 8
LDY WDATA ; 4
STA page << 8,Y ; 5
LDY WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_24"))
@ -850,7 +856,7 @@ tickident page, 8
.macro op_tick_42 page ; pattern repeats from op_tick_24
;4+(4+4+5+4+5+4+5+4+3+4)+3+24
.ident (.concat ("op_tick_42_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -860,7 +866,7 @@ tickident page, 8
STA page << 8,Y ; 5
LDY WDATA ; 4
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_24")) ; 3 + 24
.endmacro
@ -868,7 +874,7 @@ tickident page, 8
.macro op_tick_44 page ; pattern repeats from op_tick_26
; 4 + (4+4+5+4+5+4+5+4+5+4)+3+3+19
.ident (.concat ("op_tick_44_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -878,7 +884,7 @@ tickident page, 8
STA page << 8,Y ; 5
LDY WDATA ; 4
STA page << 8,Y ; 5
BIT TICK; 4
STA TICK; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_19")) ; 3 + 19
@ -887,7 +893,7 @@ tickident page, 8
.macro op_tick_46 page ; pattern repeats from op_tick_28
;4+(4+2+4+5+4+5+4+5+4+5+4)+3+20
.ident (.concat ("op_tick_46_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -898,7 +904,7 @@ tickident page, 8
LDY WDATA ; 4
STA page << 8,Y ; 5
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_20"))
.endmacro
@ -906,7 +912,7 @@ tickident page, 8
.macro op_tick_48 page ; pattern repeats from op_tick_30
;4+(4+4+5+4+5+4+5+4+5+4+4)+3+3+15
.ident (.concat ("op_tick_48_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -918,7 +924,7 @@ tickident page, 8
STA page << 8,Y ; 5
LDA WDATA ; 4
BIT TICK ; 4
STA TICK ; 4
STA zpdummy ; 3
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_15")) ; 3 + 15
@ -927,7 +933,7 @@ tickident page, 8
.macro op_tick_50 page ; pattern repeats from op_tick_32
;4+(4+4+5+4+5+4+5+4+5+4+2+4)+3+16
.ident (.concat ("op_tick_50_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -940,7 +946,7 @@ tickident page, 8
LDA WDATA ; 4
NOP ; 2
BIT TICK ; 4
STA TICK ; 4
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_16"))
.endmacro
@ -948,7 +954,7 @@ tickident page, 8
.macro op_tick_52 page ; pattern repeats from op_tick_34
;4+(4+4+5+4+5+4+5+4+5+4+4+4)+2+3+12
.ident (.concat ("op_tick_52_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -961,7 +967,7 @@ tickident page, 8
LDA WDATA ; 4
STA .ident (.concat ("_op_tick_6_page_", .string(page), "_jmp"))+2 ; 4
BIT TICK ; 4
STA TICK ; 4
NOP ; 2
JMP .ident(.concat("_op_tick_page_", .string(page), "_tail_12"))
@ -970,7 +976,7 @@ tickident page, 8
.macro op_tick_54 page ; pattern repeats from op_tick_36
; 4 + (4+4+5+4+5+4+5+3+3+4+5+4+4)+4+4+4+3
.ident (.concat ("op_tick_54_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -986,7 +992,7 @@ tickident page, 8
STA zpdummy ; 3
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
; used >3 pad cycles between tick pair; can't branch to tail
STA @D+2 ; 4
@ -999,7 +1005,7 @@ tickident page, 8
.macro op_tick_56 page
; 4+(4+4+5+4+5+4+5+4+5+4+4+4+4)+2+4+4+3
.ident (.concat ("op_tick_56_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1014,7 +1020,7 @@ tickident page, 8
STA @D+2 ; 4
STA dummy ; 4
BIT TICK ; 4
STA TICK ; 4
; used >3 pad cycles between tick pair; can't branch to tail
NOP ; 2
@ -1028,7 +1034,7 @@ tickident page, 8
.macro op_tick_58 page ; pattern repeats from op_tick_40
;4+(4+4+5+4+5+4+5+4+5+4+4+3+3+4)+4+4+3
.ident (.concat ("op_tick_58_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1044,7 +1050,7 @@ tickident page, 8
STA zpdummy ; 3
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
; used >3 pad cycles between tick pair; can't branch to tail
LDA WDATA ; 4
@ -1056,7 +1062,7 @@ tickident page, 8
.macro op_tick_60 page
; 4+(4+4+5+4+5+4+5+4+5+4+4+4+4+4)+2+4+3
.ident (.concat ("op_tick_60_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1073,7 +1079,7 @@ tickident page, 8
LDA WDATA ; 4
STA dummy ; 4
BIT TICK ; 4
STA TICK ; 4
; used >3 pad cycles between tick pair; can't branch to tail
NOP ; 2
@ -1085,7 +1091,7 @@ tickident page, 8
.macro op_tick_62 page
;4+(4+4+5+4+5+4+5+4+5+4+4+4+3+3+4)+4+3
.ident (.concat ("op_tick_62_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1102,7 +1108,7 @@ tickident page, 8
STA zpdummy ; 3
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
; used >3 pad cycles between tick pair; can't branch to tail
STA @D+1 ; 4
@ -1113,7 +1119,7 @@ tickident page, 8
.macro op_tick_64 page
;4+(4+4+5+4+5+4+5+4+5+4+4+4+4+4+4)+2+3
.ident (.concat ("op_tick_64_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1131,7 +1137,7 @@ tickident page, 8
STA @D+1 ; 4
STA dummy ; 4
BIT TICK ; 4
STA TICK ; 4
NOP ; 2
@D:
@ -1141,7 +1147,7 @@ tickident page, 8
.macro op_tick_66 page ; pattern repeats from op_tick_8
; 4+(4+4+5+4+5+4+5+4+5+4+4+4+3+4+3+4)+3
.ident (.concat ("op_tick_66_page_", .string(page))):
BIT TICK ; 4
STA TICK ; 4
LDA WDATA ; 4
LDY WDATA ; 4
STA page << 8,Y ; 5
@ -1160,7 +1166,7 @@ tickident page, 8
STA zpdummy ; 3
STA zpdummy ; 3
BIT TICK ; 4
STA TICK ; 4
@D:
JMP op_nop ; 3
@ -1278,6 +1284,7 @@ op_terminate:
LDA KBD
BPL @0
@1: ; key pressed
LDA KBDSTRB ; clear strobe
JMP exit
; Manage W5100 socket buffer and ACK TCP stream.
@ -1286,7 +1293,7 @@ op_terminate:
; the last 4 bytes in a 2K "TCP frame". i.e. we can assume that we need to consume
; exactly 2K from the W5100 socket buffer.
op_ack:
BIT TICK ; 4
STA TICK ; 4
; allow flip-flopping the PAGE1/PAGE2 soft switches to steer writes to MAIN/AUX screens
; actually this allows touching any $C0XX soft-switch, in case that is useful somehow
@ -1307,7 +1314,7 @@ op_ack:
LDX #<S0RXRD ; 2
STX WADRL ; 4
BIT TICK ; 4 (36)
STA TICK ; 4 (36)
LDA WDATA ; 4 Read high byte
; No need to read low byte since it's guaranteed to be 0 since we're at the end of a 2K frame.
@ -1332,7 +1339,7 @@ op_ack:
; - used as the low byte for resetting the W5100 address pointer when we're ready to start processing more data
LDX #$00 ; 2 restore invariant for dispatch loop
JMP checkrecv ; 3 (37 with following BIT TICK)
JMP checkrecv ; 3 (37 with following STA TICK)
; Quit to ProDOS
exit:

149
transcoder/colours.py Normal file
View File

@ -0,0 +1,149 @@
"""Apple II nominal display colours, represented by 4-bit dot sequences.
These are the "asymptotic" colours as displayed in e.g. continuous runs of
pixels. The effective colours that are actually displayed are not discrete,
due to NTSC artifacting being a continuous process.
"""
from typing import Tuple, Type
import enum
import functools
class NominalColours(enum.Enum):
pass
class HGRColours(NominalColours):
"""Map from 4-bit dot representation to DHGR pixel colours.
Dots are in memory bit order (MSB -> LSB), which is opposite to screen
order (LSB -> MSB is ordered left-to-right on the screen)
Note that these are right-rotated from the HGR mapping, because of a
1-tick phase difference in the colour reference signal for DHGR vs HGR
"""
BLACK = 0b0000
MAGENTA = 0b0001
BROWN = 0b1000
ORANGE = 0b1001 # HGR colour
DARK_GREEN = 0b0100
GREY1 = 0b0101
GREEN = 0b1100 # HGR colour
YELLOW = 0b1101
DARK_BLUE = 0b0010
VIOLET = 0b0011 # HGR colour
GREY2 = 0b1010
PINK = 0b1011
MED_BLUE = 0b0110 # HGR colour
LIGHT_BLUE = 0b0111
AQUA = 0b1110
WHITE = 0b1111
class DHGRColours(NominalColours):
"""Map from 4-bit dot representation to DHGR pixel colours.
Dots are in memory bit order (MSB -> LSB), which is opposite to screen
order (LSB -> MSB is ordered left-to-right on the screen)
Note that these are right-rotated from the HGR mapping, because of a
1-tick phase difference in the colour reference signal for DHGR vs HGR
"""
# representation.
BLACK = 0b0000
MAGENTA = 0b1000
BROWN = 0b0100
ORANGE = 0b1100 # HGR colour
DARK_GREEN = 0b0010
GREY1 = 0b1010
GREEN = 0b0110 # HGR colour
YELLOW = 0b1110
DARK_BLUE = 0b0001
VIOLET = 0b1001 # HGR colour
GREY2 = 0b0101
PINK = 0b1101
MED_BLUE = 0b0011 # HGR colour
LIGHT_BLUE = 0b1011
AQUA = 0b0111
WHITE = 0b1111
def ror(int4: int, howmany: int) -> int:
"""Rotate-right an int4 some number of times."""
res = int4
for _ in range(howmany):
res = _ror(res)
return res
def _ror(int4: int) -> int:
return ((int4 & 0b1110) >> 1) ^ ((int4 & 0b0001) << 3)
def rol(int4: int, howmany: int) -> int:
"""Rotate-left an int4 some number of times."""
res = int4
for _ in range(howmany):
res = _rol(res)
return res
def _rol(int4: int) -> int:
return ((int4 & 0b0111) << 1) ^ ((int4 & 0b1000) >> 3)
@functools.lru_cache(None)
def dots_to_nominal_colour_pixels(
num_bits: int,
dots: int,
colours: Type[NominalColours],
init_phase: int = 1 # Such that phase = 0 at start of body
) -> Tuple[NominalColours]:
"""Sequence of num_bits nominal colour pixels via sliding 4-bit window.
Includes the 3-bit header that represents the trailing 3 bits of the
previous tuple body. e.g. for DHGR, storing a byte in aux even columns
will also influence the colours of the previous main odd column.
This naively models (approximates) the NTSC colour artifacting.
TODO: Use a more careful analogue colour composition model to produce
effective pixel colours.
TODO: DHGR vs HGR colour differences can be modeled by changing init_phase
"""
res = []
shifted = dots
phase = init_phase
for i in range(num_bits):
colour = rol(shifted & 0b1111, phase)
res.append(colours(colour))
shifted >>= 1
phase += 1
if phase == 4:
phase = 0
return tuple(res)
@functools.lru_cache(None)
def dots_to_nominal_colour_pixel_values(
num_bits: int,
dots: int,
colours: Type[NominalColours],
init_phase: int = 1 # Such that phase = 0 at start of body
) -> Tuple[int]:
""""Sequence of num_bits nominal colour values via sliding 4-bit window."""
return tuple(p.value for p in dots_to_nominal_colour_pixels(
num_bits, dots, colours, init_phase
))

113
transcoder/colours_test.py Normal file
View File

@ -0,0 +1,113 @@
import unittest
import colours
HGRColours = colours.HGRColours
class TestColours(unittest.TestCase):
def test_dots_to_pixels(self):
self.assertEqual(
(
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.DARK_BLUE,
HGRColours.MED_BLUE,
HGRColours.AQUA,
HGRColours.AQUA,
HGRColours.GREEN,
HGRColours.BROWN,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK
),
colours.dots_to_nominal_colour_pixels(
31, 0b00000000000000000000111000000000, HGRColours, init_phase=0
)
)
self.assertEqual(
(
HGRColours.BLACK,
HGRColours.MAGENTA,
HGRColours.VIOLET,
HGRColours.LIGHT_BLUE,
HGRColours.WHITE,
HGRColours.AQUA,
HGRColours.GREEN,
HGRColours.BROWN,
HGRColours.BLACK,
HGRColours.MAGENTA,
HGRColours.VIOLET,
HGRColours.LIGHT_BLUE,
HGRColours.WHITE,
HGRColours.AQUA,
HGRColours.GREEN,
HGRColours.BROWN,
HGRColours.BLACK,
HGRColours.MAGENTA,
HGRColours.VIOLET,
HGRColours.LIGHT_BLUE,
HGRColours.WHITE,
HGRColours.AQUA,
HGRColours.GREEN,
HGRColours.BROWN,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK,
HGRColours.BLACK
),
colours.dots_to_nominal_colour_pixels(
31, 0b0000111100001111000011110000, HGRColours, init_phase=0
)
)
class TestRolRoR(unittest.TestCase):
def testRolOne(self):
self.assertEqual(0b1111, colours.rol(0b1111, 1))
self.assertEqual(0b0001, colours.rol(0b1000, 1))
self.assertEqual(0b1010, colours.rol(0b0101, 1))
def testRolMany(self):
self.assertEqual(0b1111, colours.rol(0b1111, 3))
self.assertEqual(0b0010, colours.rol(0b1000, 2))
self.assertEqual(0b0101, colours.rol(0b0101, 2))
def testRorOne(self):
self.assertEqual(0b1111, colours.ror(0b1111, 1))
self.assertEqual(0b1000, colours.ror(0b0001, 1))
self.assertEqual(0b0101, colours.ror(0b1010, 1))
def testRoRMany(self):
self.assertEqual(0b1111, colours.ror(0b1111, 3))
self.assertEqual(0b1000, colours.ror(0b0010, 2))
self.assertEqual(0b0101, colours.ror(0b0101, 2))
if __name__ == "__main__":
unittest.main()

1
transcoder/data/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.bz2 filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b47eadfdf8c8e16c6539f9a16ed0b5a393b17e0cbd03831aacda7f659e9522d6
size 120830327

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c245981f91ffa89b47abdd1c9d646c2e79499a0c82c38c91234be0a59e52f1f
size 118832545

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3fd52feb08eb6f99b267a1050c68905f25d0d106ad7c2c63473cc0a0f6aa1b25
size 224334626

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbf83e3d0b6c7867ccf7ae1d55a6ed4e906409b08043dec514e1104cec95f0fc
size 220565577

View File

@ -1,310 +0,0 @@
"""Computes visual differences between screen image data.
This is the core of the video encoding, for three reasons:
- The edit distance between old and new frames is used to prioritize which
screen bytes to send
- When deciding which other offset bytes to send along with a chosen screen
byte, we minimize the error introduced by sending this (probably non-optimal)
byte instead of the actual target screen byte. This needs to account for the
colour artifacts introduced by this byte as well as weighting perceived
errors introduced (e.g. long runs of colour)
- The byte_screen_error_distance function is on the critical path of the encoding.
"""
import functools
import numpy as np
import weighted_levenshtein
@functools.lru_cache(None)
def byte_to_nominal_colour_string(b: int, is_odd_offset: bool) -> str:
"""Compute nominal pixel colours for a byte.
This ignores any fringing/colour combining effects, as well as
half-ignoring what happens to the colour pixel that crosses the byte
boundary.
A better implementation of this might be to consider neighbouring (even,
odd) column bytes together since this will allow correctly colouring the
split pixel in the middle.
There are also even weirder colour artifacts that happen when
neighbouring bytes have mismatched colour palettes, which also cross the
odd/even boundary. But these may not be worth worrying about.
:param b: byte to encode
:param is_odd_offset: whether byte is at an odd screen column
:return: string encoding nominal colour of pixels in the byte, with "0"
or "1" for the "hanging" bit that spans the neighbouring byte.
"""
pixels = []
idx = 0
if is_odd_offset:
pixels.append("01"[b & 0x01])
idx += 1
# K = black
# G = green
# V = violet
# W = white
palettes = (
(
"K", # 0x00
"V", # 0x01
"G", # 0x10
"W" # 0x11
), (
"K", # 0x00
"B", # 0x01
"O", # 0x10
"W" # 0x11
)
)
palette = palettes[(b & 0x80) != 0]
for _ in range(3):
pixel = palette[(b >> idx) & 0b11]
pixels.append(pixel)
idx += 2
if not is_odd_offset:
pixels.append("01"[(b & 0x40) != 0])
idx += 1
return "".join(pixels)
@functools.lru_cache(None)
def byte_to_colour_string_with_white_coalescing(
b: int, is_odd_offset: bool) -> str:
"""Model the combining of neighbouring 1 bits to produce white.
The output is a string of length 7 representing the 7 display dots that now
have colour.
Attempt to model the colour artifacting that consecutive runs of
1 bits are coerced to white. This isn't quite correct since:
a) it doesn't operate across byte boundaries (see note on
byte_to_nominal_colour_string)
b) a sequence like WVV appears more like WWWVVV or WWVVVV rather than WWWKVV
(at least on the //gs)
It also ignores other colour fringing e.g. from NTSC artifacts.
TODO: this needs more work.
:param b:
:param is_odd_offset:
:return:
"""
pixels = []
fringing = {
"1V": "WWK", # 110
"1W": "WWW", # 111
"1B": "WWB", # 110
"WV": "WWWK", # 1110
"WB": "WWWK", # 1110
"GV": "KWWK", # 0110
"OB": "KWWK", # 0110
"GW": "KWWW", # 0111
"OW": "KWWW", # 0111
"W1": "WWW", # 111
"G1": "KWW", # 011
"O1": "KWW", # 011
}
nominal = byte_to_nominal_colour_string(b, is_odd_offset)
for idx in range(3):
pair = nominal[idx:idx + 2]
effective = fringing.get(pair)
if not effective:
e = []
if pair[0] in {"0", "1"}:
e.append(pair[0])
else:
e.extend([pair[0], pair[0]])
if pair[1] in {"0", "1"}:
e.append(pair[1])
else:
e.extend([pair[1], pair[1]])
effective = "".join(e)
if pixels:
pixels.append(effective[2:])
else:
pixels.append(effective)
return "".join(pixels)
substitute_costs = np.ones((128, 128), dtype=np.float64)
# Substitution costs to use when evaluating other potential offsets at which
# to store a content byte. We penalize more harshly for introducing
# errors that alter pixel colours, since these tend to be very
# noticeable as visual noise.
error_substitute_costs = np.ones((128, 128), dtype=np.float64)
# Penalty for turning on/off a black bit
for c in "01GVWOB":
substitute_costs[(ord('K'), ord(c))] = 1
substitute_costs[(ord(c), ord('K'))] = 1
error_substitute_costs[(ord('K'), ord(c))] = 5
error_substitute_costs[(ord(c), ord('K'))] = 5
# Penalty for changing colour
for c in "01GVWOB":
for d in "01GVWOB":
substitute_costs[(ord(c), ord(d))] = 1
substitute_costs[(ord(d), ord(c))] = 1
error_substitute_costs[(ord(c), ord(d))] = 5
error_substitute_costs[(ord(d), ord(c))] = 5
insert_costs = np.ones(128, dtype=np.float64) * 1000
delete_costs = np.ones(128, dtype=np.float64) * 1000
def _edit_weight(a: int, b: int, is_odd_offset: bool, error: bool):
"""
:param a:
:param b:
:param is_odd_offset:
:param error:
:return:
"""
a_pixels = byte_to_colour_string_with_white_coalescing(a, is_odd_offset)
b_pixels = byte_to_colour_string_with_white_coalescing(b, is_odd_offset)
dist = weighted_levenshtein.dam_lev(
a_pixels, b_pixels,
insert_costs=insert_costs,
delete_costs=delete_costs,
substitute_costs=error_substitute_costs if error else substitute_costs,
)
return np.int64(dist)
@functools.lru_cache(None)
def _edit_weight_matrices(error: bool) -> np.array:
"""
:param error:
:return:
"""
ewm = np.zeros(shape=(256, 256, 2), dtype=np.int64)
for a in range(256):
for b in range(256):
for is_odd_offset in (False, True):
ewm[a, b, int(is_odd_offset)] = _edit_weight(
a, b, is_odd_offset, error)
return ewm
@functools.lru_cache(None)
def edit_weight(a: int, b: int, is_odd_offset: bool, error: bool):
"""
:param a: first content value
:param b: second content value
:param is_odd_offset: whether this content byte is at an odd screen
byte offset
:param error: whether to compute error distance or edit distance
:return: the corresponding distance value
"""
return _edit_weight_matrices(error)[a, b, int(is_odd_offset)]
_even_ewm = {}
_odd_ewm = {}
_even_error_ewm = {}
_odd_error_ewm = {}
for a in range(256):
for b in range(256):
_even_ewm[(a << 8) + b] = edit_weight(a, b, False, False)
_odd_ewm[(a << 8) + b] = edit_weight(a, b, True, False)
_even_error_ewm[(a << 8) + b] = edit_weight(a, b, False, True)
_odd_error_ewm[(a << 8) + b] = edit_weight(a, b, True, True)
@functools.lru_cache(None)
def _constant_array(content: int, shape) -> np.array:
"""
:param content:
:param shape:
:return:
"""
return np.ones(shape, dtype=np.uint16) * content
def byte_screen_error_distance(content: int, b: np.array) -> np.array:
"""
:param content: byte for which to compute error distance
:param b: np.array of size (32, 256) representing existing screen memory.
:return: np.array of size (32, 256) representing error distance from
content byte to each byte of b
"""
assert b.shape == (32, 256), b.shape
# Extract even and off column offsets (128,)
even_b = b[:, ::2]
odd_b = b[:, 1::2]
a = _constant_array(content << 8, even_b.shape)
even = a + even_b
odd = a + odd_b
even_weights = np.vectorize(_even_error_ewm.__getitem__)(even)
odd_weights = np.vectorize(_odd_error_ewm.__getitem__)(odd)
res = np.ndarray(shape=b.shape, dtype=np.int64)
res[:, ::2] = even_weights
res[:, 1::2] = odd_weights
return res
def screen_edit_distance(a: np.array, b: np.array) -> np.array:
"""
:param a:
:param b:
:return:
"""
# Extract even and off column offsets (32, 128)
even_a = a[:, ::2]
odd_a = a[:, 1::2]
even_b = b[:, ::2]
odd_b = b[:, 1::2]
even = (even_a.astype(np.uint16) << 8) + even_b
odd = (odd_a.astype(np.uint16) << 8) + odd_b
even_weights = np.vectorize(_even_ewm.__getitem__)(even)
odd_weights = np.vectorize(_odd_ewm.__getitem__)(odd)
res = np.ndarray(shape=a.shape, dtype=np.int64)
res[:, ::2] = even_weights
res[:, 1::2] = odd_weights
return res

View File

@ -1,88 +0,0 @@
"""Tests for the edit_distance module."""
import unittest
import edit_distance
class TestByteToNominalColourString(unittest.TestCase):
def testEncoding(self):
self.assertEqual(
"KKK0",
edit_distance.byte_to_nominal_colour_string(
0, is_odd_offset=False))
self.assertEqual(
"0KKK",
edit_distance.byte_to_nominal_colour_string(
0, is_odd_offset=True))
self.assertEqual(
"WWW1", edit_distance.byte_to_nominal_colour_string(
0xff, is_odd_offset=False))
self.assertEqual(
"1WWW", edit_distance.byte_to_nominal_colour_string(
0xff, is_odd_offset=True))
self.assertEqual(
"GGG0", edit_distance.byte_to_nominal_colour_string(
0x2a, is_odd_offset=False))
self.assertEqual(
"1GGG", edit_distance.byte_to_nominal_colour_string(
0x55, is_odd_offset=True))
self.assertEqual(
"OOO0", edit_distance.byte_to_nominal_colour_string(
0xaa, is_odd_offset=False))
self.assertEqual(
"1OOO", edit_distance.byte_to_nominal_colour_string(
0xd5, is_odd_offset=True))
class TestEditWeight(unittest.TestCase):