; =============================================================================
; CBS Cruncher v5.3 + v5.4 + v5.5 - universal direct-VRAM decruncher v0.9
; =============================================================================
; Restores the line-aware v5.5 payload or the unrestricted v5.3/v5.4
; state-flow payload directly into VRAM. v5.3/v5.4 destination 2 is a linear
; byte stream; destination 5 and v5.5 are bitmap rectangles. No complete
; expanded-data RAM buffer is required.
;
; Input:
;   HL = three-byte CBS header
;        byte 0 = C5h (v5.5), 45h/4Dh (v5.3/v5.4 rectangle), or
;                 42h/4Ah (v5.3/v5.4 linear VRAM)
;        destination 5: bytes 1-2 = packed width and height; 0 means 256
;        destination 2: bytes 1-2 = packed file length; EOS ends expansion
;   DE = packed destination coordinate: D = packed X, E = Y
;   A  = bits 3-2 VRAM command page, bits 1-0 source route
;
; Source route 0 reads continuous RAM or ROM with LD A,(HL). Literal runs use
; the CPU VRAM port. Every previous/new-offset match is split at source and
; destination row boundaries. A fragment is never longer than its distance, so
; overlapping LZ matches only read bytes that are already complete in VRAM.
;
; Offset-1 matches read the preceding packed VRAM byte once. Short runs are
; repeated directly through the CPU VRAM port; long runs use HMMV fragments or
; complete multi-line rectangles. Other match fragments up to eight bytes use
; a dedicated scratch area. Larger fragments use HMMM.
;
; Every new byte offset is divided by the packed row width once and cached as a
; row distance plus column distance. Match fragments then derive source X/Y
; directly from the current destination coordinate. Whole-row v5.5 offsets
; already carry their row distance and bypass the division completely.
;
; Width, columns, lengths and offsets are measured in packed bytes. Conversion
; to pixels happens only while VDP command registers are emitted. HMMM and HMMV
; coordinates are sent directly with OUT (C),register; there is no command RAM
; block and no OTIR command setup.
;
	ORG	E000h

; Fixed decoder routing thresholds. They are not calculated from the image list.
; Offset-1 runs of 64+ bytes use the VDP fill path; matches of 1-8 bytes use
; the small CPU scratch path, while larger matches use VDP-to-VDP copying.
CBS_VD_FAST_FILL_MIN:		EQU	40h
CBS_VD_TINY_MATCH_MAX:		EQU	08h

; Public entry table:
;   E000h / CBS_INIT_MSX    call exactly once after copying the decoder to RAM
;   E003h / CBS_INIT_SCREEN A = 5, 6, 7 or 8; call when Screen mode changes
;   E006h / CBS_DECRUNCH    rectangle or linear-VRAM decrunch entry
;   E009h / CBS_WAIT_VDP    wait until the current VDP command finishes
;
; CBS_INIT_MSX reads VDP.DR/VDP.DW from BIOS addresses 0006h/0007h and patches
; all immediate I/O operands. CBS_INIT_SCREEN patches the pixel, line and page
; shifts. Neither initializer is executed inside the token or match loops.
CBS_INIT_MSX:
	JP	CBS_VD_INIT_MSX
CBS_INIT_SCREEN:
	JP	CBS_VD_INIT_SCREEN
CBS_DECRUNCH:
	JP	CBS_V5354_DIRECT_VRAM
CBS_WAIT_VDP:
	JP	CBS_VD_WAIT_COMMAND

CBS_V5354_DIRECT_VRAM:
	AND	Ch
	RRCA
	RRCA
	LD	(CBS_VD_CURRENT_Y+1),A
	LD	A,E
	LD	(CBS_VD_CURRENT_Y),A
	LD	A,D
	LD	(CBS_VD_START_X),A
	XOR	A
	LD	(CBS_VD_START_X+1),A
	LD	(CBS_VD_COLUMN),A
	LD	(CBS_VD_COLUMN+1),A

	LD	A,(HL)
	AND	C0h
	LD	DE,CBS_VD_STATE_START
	CP	C0h
	JR	NZ,CBS_VD_SET_START
	LD	DE,CBS_VD_ROW_START
CBS_VD_SET_START:
	LD	(CBS_VD_START_JUMP+1),DE

	LD	A,(HL)
	AND	08h			; clear=v5.3, set=v5.4
	XOR	08h			; 8 skips selector, 0 executes it
	LD	(CBS_VD_VARIANT_JUMP+1),A

	LD	A,(HL)
	AND	07h
	CP	02h
	JR	Z,CBS_VD_LINEAR_HEADER

	INC	HL			; skip format/destination byte
	LD	A,(HL)
	INC	HL
	PUSH	HL			; preserve height-byte address
	LD	L,A
	LD	H,00h
	OR	A
	JR	NZ,CBS_VD_WIDTH_READY
	INC	H			; zero width means 256 bytes

CBS_VD_WIDTH_READY:
	LD	(CBS_VD_WIDTH),HL
	POP	HL
	INC	HL			; skip height; HL = payload
	JR	CBS_VD_HEADER_READY

CBS_VD_LINEAR_HEADER:
	LD	A,(CBS_VD_START_X)			; D supplied the initial packed X
	LD	(CBS_VD_COLUMN),A
	XOR	A
	LD	(CBS_VD_START_X),A
	LD	DE,0080h			; patched to the physical Screen width
CBS_VD_LINEAR_WIDTH:		EQU	$ - 2
	LD	(CBS_VD_WIDTH),DE
	INC	HL
	INC	HL
	INC	HL			; skip route and packed-length bytes

CBS_VD_HEADER_READY:
	XOR	A
	LD	(CBS_VD_COMMAND_ACTIVE),A
	LD	BC,FFFFh
	CALL	CBS_VD_STORE_OFFSET			; default offset = -1
	LD	A,80h			; control-bit sentinel

	DI
CBS_VD_START_JUMP:
	JP	CBS_VD_STATE_START

CBS_VD_STATE_START:
	CALL	CBS_VD_ELIAS			; stream begins with literals
	JR	CBS_VD_COPY_LITERALS

CBS_VD_AFTER_LITERAL:
	CALL	CBS_VD_GET_BIT			; previous offset or new offset?
	JR	C,CBS_VD_NEW_OFFSET
	CALL	CBS_VD_ELIAS
	JR	CBS_VD_COPY_MATCH

CBS_VD_AFTER_MATCH:
	CALL	CBS_VD_GET_BIT			; literals or new offset?
	JR	C,CBS_VD_NEW_OFFSET
	CALL	CBS_VD_ELIAS

CBS_VD_COPY_LITERALS:
	LD	(CBS_VD_REMAIN),BC
	CALL	CBS_VD_WRITE_LITERALS
	JR	CBS_VD_AFTER_LITERAL

CBS_VD_COPY_MATCH:
	LD	(CBS_VD_REMAIN),BC
	PUSH	AF
	PUSH	HL			; preserve packed-stream pointer
CBS_VD_STATE_MATCH_CALL:
	CALL	CBS_VD_COPY_MATCH_LOOP
	POP	HL
	POP	AF
	JR	CBS_VD_AFTER_MATCH

CBS_VD_NEW_OFFSET:

; The selector reader is exactly eight bytes. The patched JR displacement is
; 8 for v5.3 and 0 for v5.4.
CBS_VD_VARIANT_JUMP:
	JR	CBS_VD_SELECTOR			; assembled displacement = 0

CBS_VD_SELECTOR:
	CALL	CBS_VD_GET_BIT
	JR	C,CBS_VD_GENERAL_OFFSET
	JP	CBS_VD_NEAR_OFFSET

CBS_VD_GENERAL_OFFSET:
	LD	C,FEh
	CALL	CBS_VD_ELIAS_LOOP
	INC	C
	JP	Z,CBS_VD_FINISHED			; impossible high 256 = EOS
	LD	B,C
	LD	C,(HL)
	INC	HL
	RR	B
	RR	C			; Carry = first length control bit
	CALL	CBS_VD_STORE_OFFSET
	LD	BC,0001h
	CALL	NC,CBS_VD_ELIAS_BACKTRACK
	INC	BC
	JR	CBS_VD_COPY_MATCH

CBS_VD_NEAR_OFFSET:
	LD	C,FEh
	CALL	CBS_VD_ELIAS_LOOP
	INC	C
	LD	B,FFh
	CALL	CBS_VD_STORE_OFFSET
	LD	BC,0000h
	CALL	CBS_VD_ELIAS
	INC	BC
	JR	CBS_VD_COPY_MATCH

; -----------------------------------------------------------------------------
; C5h uses the complete v5.5 grammar: ordinary literals, previous/general
; offsets and the whole-line-distance shortcut. It shares the same unrestricted
; VRAM match backend as 45h/4Dh.
; -----------------------------------------------------------------------------
CBS_VD_ROW_START:
	CALL	CBS_VD_ELIAS
	JR	CBS_VD_ROW_COPY_LITERALS

CBS_VD_ROW_AFTER_LITERAL:
	CALL	CBS_VD_GET_BIT
	JR	C,CBS_VD_ROW_NEW_OFFSET
	CALL	CBS_VD_ELIAS
	CALL	CBS_VD_IS_LENGTH_ONE
	JR	NC,CBS_VD_ROW_COPY_MATCH
	CALL	CBS_VD_GET_BIT
	JP	C,CBS_VD_FINISHED
	LD	BC,0001h
	JR	CBS_VD_ROW_COPY_MATCH

CBS_VD_ROW_AFTER_MATCH:
	CALL	CBS_VD_GET_BIT
	JR	C,CBS_VD_ROW_NEW_OFFSET
	CALL	CBS_VD_ELIAS
	CALL	CBS_VD_IS_LENGTH_ONE
	JR	NC,CBS_VD_ROW_COPY_LITERALS
	CALL	CBS_VD_GET_BIT
	JP	C,CBS_VD_FINISHED
	LD	BC,0001h

CBS_VD_ROW_COPY_LITERALS:
	LD	(CBS_VD_REMAIN),BC
	CALL	CBS_VD_WRITE_LITERALS
	JR	CBS_VD_ROW_AFTER_LITERAL

CBS_VD_ROW_COPY_MATCH:
	LD	(CBS_VD_REMAIN),BC
	PUSH	AF
	PUSH	HL
CBS_VD_ROW_MATCH_CALL:
	CALL	CBS_VD_COPY_MATCH_LOOP
	POP	HL
	POP	AF
	JR	CBS_VD_ROW_AFTER_MATCH

CBS_VD_ROW_NEW_OFFSET:
	CALL	CBS_VD_GET_BIT
	JR	C,CBS_VD_ROW_GENERAL_OFFSET

	LD	BC,FFFEh
	CALL	CBS_VD_ELIAS_LOOP
	INC	BC			; negative whole-line distance
	PUSH	AF
	PUSH	HL
	LD	HL,0000h
	OR	A
	SBC	HL,BC
	LD	(CBS_VD_OFFSET_ROWS),HL
	LD	HL,0000h
	LD	(CBS_VD_OFFSET_COLUMNS),HL
	LD	DE,(CBS_VD_WIDTH)
	LD	HL,0000h
	LD	A,10h
CBS_VD_ROW_MULTIPLY:
	ADD	HL,HL
	SLA	C
	RL	B
	JR	NC,CBS_VD_ROW_MULTIPLY_SKIP
	ADD	HL,DE
CBS_VD_ROW_MULTIPLY_SKIP:
	DEC	A
	JR	NZ,CBS_VD_ROW_MULTIPLY
	LD	B,H
	LD	C,L			; negative byte distance
	POP	HL
	POP	AF
	PUSH	HL
	PUSH	AF
	LD	HL,0000h
	OR	A
	SBC	HL,BC
	LD	(CBS_VD_DISTANCE),HL
	CALL	CBS_VD_SELECT_MATCH_ROUTE
	POP	AF
	POP	HL
	CALL	CBS_VD_ELIAS
	INC	BC
	JR	CBS_VD_ROW_COPY_MATCH

CBS_VD_ROW_GENERAL_OFFSET:
	LD	C,FEh
	CALL	CBS_VD_ELIAS_LOOP
	INC	C
	LD	B,C
	LD	C,(HL)
	INC	HL
	RR	B
	RR	C
	CALL	CBS_VD_STORE_OFFSET
	LD	BC,0001h
	CALL	NC,CBS_VD_ELIAS_BACKTRACK
	INC	BC
	JR	CBS_VD_ROW_COPY_MATCH

CBS_VD_IS_LENGTH_ONE:
	PUSH	AF
	LD	A,B
	OR	A
	JR	NZ,CBS_VD_LENGTH_NOT_ONE
	LD	A,C
	DEC	A
	JR	NZ,CBS_VD_LENGTH_NOT_ONE
	POP	AF
	SCF
	RET
CBS_VD_LENGTH_NOT_ONE:
	POP	AF
	OR	A
	RET

CBS_VD_FINISHED:
; -----------------------------------------------------------------------------
; Only another VDP command or decoder exit must wait for CE to clear. Literal
; output through port 98h may continue while an earlier HMMM is active.
; -----------------------------------------------------------------------------
CBS_VD_WAIT_IF_COMMAND:
	LD	A,00h
CBS_VD_COMMAND_ACTIVE:		EQU	$ - 1
	OR	A
	RET	Z

	LD	A,02h
CBS_VD_PORT1_STATUS_INDEX:
	OUT	(99h),A
	LD	A,8Fh
CBS_VD_PORT1_STATUS_SELECT:
	OUT	(99h),A

CBS_VD_WAIT_COMMAND_LOOP:
CBS_VD_PORT1_STATUS_READ:
	IN	A,(99h)
	RRCA
	JR	C,CBS_VD_WAIT_COMMAND_LOOP
	XOR	A
CBS_VD_PORT1_STATUS_ZERO:
	OUT	(99h),A
	LD	A,8Fh
CBS_VD_PORT1_STATUS_RESTORE:
	OUT	(99h),A
	XOR	A
	LD	(CBS_VD_COMMAND_ACTIVE),A
	RET

; Public unconditional wait entry. The controller enters with interrupts
; disabled, so each poll selects status 2 atomically, restores status 0, then
; opens an interrupt window while the command engine continues. Return with
; interrupts enabled so a following HALT can expose one complete black frame.
CBS_VD_WAIT_COMMAND:
	LD	A,(0007h)		; BIOS VDP write base port
	INC	A
	LD	C,A			; C = VDP register/status port
CBS_VD_WAIT_PUBLIC_LOOP:
	DI
	LD	A,02h
	OUT	(C),A
	LD	A,8Fh
	OUT	(C),A			; select status register 2
	IN	A,(C)
	RRCA				; Carry = CE
	LD	A,00h			; preserve Carry while restoring R15
	OUT	(C),A
	LD	A,8Fh
	EI				; takes effect after the following OUT
	OUT	(C),A			; restore status register 0 atomically
	JR	C,CBS_VD_WAIT_PUBLIC_LOOP
	XOR	A
	LD	(CBS_VD_COMMAND_ACTIVE),A
	RET

; -----------------------------------------------------------------------------
; Store a negative byte offset in BC. General offsets are divided once into a
; positive row distance and column distance. AF, including the length backtrack
; Carry, is preserved.
; -----------------------------------------------------------------------------
CBS_VD_STORE_OFFSET:
	PUSH	HL			; preserve packed-stream pointer
	PUSH	AF
	LD	HL,0000h
	OR	A
	SBC	HL,BC
	LD	(CBS_VD_DISTANCE),HL
	LD	DE,(CBS_VD_WIDTH)
	LD	BC,0000h
	LD	A,10h

; Divide the positive distance in HL by the positive packed width in DE.
; Quotient rows remain in HL and remainder columns in BC. The fixed sixteen
; iterations avoid a new data-dependent loop for narrow images.
CBS_VD_DIVIDE_LOOP:
	ADD	HL,HL
	RL	C
	RL	B
	PUSH	HL
	LD	H,B
	LD	L,C
	OR	A
	SBC	HL,DE
	JR	C,CBS_VD_DIVIDE_RESTORE
	LD	B,H
	LD	C,L
	POP	HL
	INC	L
	JR	CBS_VD_DIVIDE_NEXT

CBS_VD_DIVIDE_RESTORE:
	POP	HL

CBS_VD_DIVIDE_NEXT:
	DEC	A
	JR	NZ,CBS_VD_DIVIDE_LOOP
	LD	(CBS_VD_OFFSET_ROWS),HL
	LD	H,B
	LD	L,C
	LD	(CBS_VD_OFFSET_COLUMNS),HL
	CALL	CBS_VD_SELECT_MATCH_ROUTE

	POP	AF
	POP	HL
	RET

; Whole-row v5.5 offsets cache their geometry before arriving here. Both new
; offset paths call this selector, so neither keeps a private one-call helper.
CBS_VD_SELECT_MATCH_ROUTE:
	LD	HL,(CBS_VD_DISTANCE)
	LD	DE,CBS_VD_COPY_MATCH_LOOP
	DEC	HL
	LD	A,H
	OR	L
	JR	NZ,CBS_VD_STORE_MATCH_ROUTE
	LD	DE,CBS_VD_COPY_OFFSET1
CBS_VD_STORE_MATCH_ROUTE:
	LD	(CBS_VD_STATE_MATCH_CALL+1),DE
	LD	(CBS_VD_ROW_MATCH_CALL+1),DE
	RET

; -----------------------------------------------------------------------------
; Emit a literal run through port 98h. Each OTIR ends at a destination row
; boundary, and B=0 deliberately represents 256 bytes.
; -----------------------------------------------------------------------------
CBS_VD_WRITE_LITERALS:
	PUSH	AF

CBS_VD_LITERAL_LOOP:
	PUSH	HL
	CALL	CBS_VD_CHOOSE_DEST_CHUNK
	CALL	CBS_VD_SET_WRITE_ADDRESS
	POP	HL
	LD	BC,0000h
CBS_VD_CHUNK:			EQU	$ - 2
	LD	B,C
CBS_VD_PORT0_LITERAL:
	LD	C,98h
	OTIR
	PUSH	HL
	CALL	CBS_VD_ADVANCE
	CALL	CBS_VD_SUBTRACT_CHUNK
	POP	HL
	JR	NZ,CBS_VD_LITERAL_LOOP
	POP	AF
	RET

; -----------------------------------------------------------------------------
; Copy a match from already expanded VRAM. Recalculate the source coordinate
; for each fragment because an overlapping match may consume its own earlier
; fragments. Chunk <= distance guarantees LDIR-style forward propagation.
; -----------------------------------------------------------------------------
CBS_VD_COPY_OFFSET1:
	LD	HL,(CBS_VD_REMAIN)
	LD	A,H
	OR	A
	JR	NZ,CBS_VD_OFFSET1_READ
	LD	A,L
	CP	CBS_VD_FAST_FILL_MIN
	JR	NC,CBS_VD_OFFSET1_READ

; Read the preceding packed VRAM byte once for either CPU repetition or HMMV.
CBS_VD_OFFSET1_READ:
	CALL	CBS_VD_WAIT_IF_COMMAND
	LD	HL,(CBS_VD_CURRENT_Y)
	LD	DE,(CBS_VD_COLUMN)
	LD	A,D
	OR	E
	JR	NZ,CBS_VD_OFFSET1_PREVIOUS_COLUMN
	DEC	HL
	LD	DE,(CBS_VD_WIDTH)
	DEC	DE			; preceding row ends at width-1
	JR	CBS_VD_OFFSET1_PREVIOUS_COORDINATE

CBS_VD_OFFSET1_PREVIOUS_COLUMN:
	DEC	DE

CBS_VD_OFFSET1_PREVIOUS_COORDINATE:
	LD	B,H			; selected VRAM page
	LD	H,00h			; Y within page
CBS_VD_OFFSET1_READ_SCREEN_LINE_SHIFT:
	LD	A,07h			; patched by CBS_INIT_SCREEN

CBS_VD_OFFSET1_READ_LINE_SHIFT:
	ADD	HL,HL
	DEC	A
	JR	NZ,CBS_VD_OFFSET1_READ_LINE_SHIFT
	PUSH	DE
	LD	DE,(CBS_VD_START_X)
	ADD	HL,DE
	POP	DE
	ADD	HL,DE

	PUSH	HL
	LD	A,H
	AND	C0h
	RLCA
	RLCA
	LD	C,A
	LD	A,B
CBS_VD_OFFSET1_READ_SCREEN_PAGE_SHIFT:
	LD	B,01h			; patched by CBS_INIT_SCREEN

CBS_VD_OFFSET1_READ_PAGE_SHIFT:
	ADD	A,A
	DJNZ	CBS_VD_OFFSET1_READ_PAGE_SHIFT
	OR	C
CBS_VD_PORT1_OFFSET1_R14_VALUE:
	OUT	(99h),A
	LD	A,8Eh
CBS_VD_PORT1_OFFSET1_R14_SELECT:
	OUT	(99h),A
	POP	HL
	LD	A,L
CBS_VD_PORT1_OFFSET1_ADDRESS_LOW:
	OUT	(99h),A
	LD	A,H
	AND	3Fh
CBS_VD_PORT1_OFFSET1_ADDRESS_HIGH:
	OUT	(99h),A
CBS_VD_PORT0_OFFSET1_READ:
	IN	A,(98h)
	LD	(CBS_VD_FILL_COLOR_LOAD+1),A

	LD	HL,(CBS_VD_REMAIN)
	LD	A,H
	OR	A
	JR	NZ,CBS_VD_FAST_FILL_LOOP
	LD	A,L
	CP	CBS_VD_FAST_FILL_MIN
	JR	NC,CBS_VD_FAST_FILL_LOOP

; Short offset-1 matches are cheaper as direct CPU repetitions than as a
; sequence of one-byte HMMM commands.
CBS_VD_SHORT_FILL_LOOP:
	CALL	CBS_VD_CHOOSE_DEST_CHUNK
	CALL	CBS_VD_SET_WRITE_ADDRESS
	LD	A,(CBS_VD_CHUNK)
	LD	B,A
CBS_VD_PORT0_SHORT_FILL:
	LD	C,98h
CBS_VD_FILL_COLOR_LOAD:
	LD	A,00h
CBS_VD_SHORT_FILL_BYTE:
	OUT	(C),A
	DJNZ	CBS_VD_SHORT_FILL_BYTE
	CALL	CBS_VD_ADVANCE
	CALL	CBS_VD_SUBTRACT_CHUNK
	JR	NZ,CBS_VD_SHORT_FILL_LOOP
	RET

CBS_VD_FAST_FILL_LOOP:
	CALL	CBS_VD_WAIT_IF_COMMAND
	LD	HL,(CBS_VD_COLUMN)
	LD	A,H
	OR	L
	JR	NZ,CBS_VD_FAST_FILL_FRAGMENT

	LD	HL,(CBS_VD_REMAIN)
	LD	DE,(CBS_VD_WIDTH)
	LD	BC,0000h

CBS_VD_FILL_ROW_COUNT:
	OR	A
	SBC	HL,DE
	JR	C,CBS_VD_FILL_ROWS_READY
	INC	BC
	JR	CBS_VD_FILL_ROW_COUNT

CBS_VD_FILL_ROWS_READY:
	ADD	HL,DE			; HL = trailing partial-row bytes
	LD	A,B
	OR	C
	JR	Z,CBS_VD_FAST_FILL_FRAGMENT

	PUSH	BC			; preserve complete-row count
	LD	DE,(CBS_VD_REMAIN)
	EX	DE,HL			; HL=remaining, DE=trailing bytes
	OR	A
	SBC	HL,DE			; HL=packed bytes in full rows
	LD	(CBS_VD_FILL_ADVANCE),HL
	LD	HL,(CBS_VD_WIDTH)
	LD	(CBS_VD_HMMV_NX),HL
	POP	BC
	LD	(CBS_VD_HMMV_NY),BC
	CALL	CBS_VD_START_HMMV
	LD	HL,(CBS_VD_CURRENT_Y)
	LD	DE,(CBS_VD_HMMV_NY)
	ADD	HL,DE
	LD	(CBS_VD_CURRENT_Y),HL
	LD	HL,0000h
CBS_VD_FILL_ADVANCE:		EQU	$ - 2
	LD	(CBS_VD_CHUNK),HL
	JR	CBS_VD_FAST_FILL_SUBTRACT

CBS_VD_FAST_FILL_FRAGMENT:
	CALL	CBS_VD_CHOOSE_DEST_CHUNK
	LD	HL,(CBS_VD_CHUNK)
	LD	(CBS_VD_HMMV_NX),HL
	LD	HL,0001h
	LD	(CBS_VD_HMMV_NY),HL
	CALL	CBS_VD_START_HMMV
	CALL	CBS_VD_ADVANCE

CBS_VD_FAST_FILL_SUBTRACT:
	CALL	CBS_VD_SUBTRACT_CHUNK
	JR	NZ,CBS_VD_FAST_FILL_LOOP
	RET

CBS_VD_COPY_MATCH_LOOP:
	LD	HL,(CBS_VD_COLUMN)
	LD	DE,0000h
CBS_VD_OFFSET_COLUMNS:		EQU	$ - 2
	LD	BC,0000h
	OR	A
	SBC	HL,DE
	JR	NC,CBS_VD_SOURCE_COLUMN_READY
	LD	DE,0000h
CBS_VD_WIDTH:			EQU	$ - 2
	ADD	HL,DE
	INC	BC

CBS_VD_SOURCE_COLUMN_READY:
	LD	(CBS_VD_SOURCE_COLUMN),HL
	LD	HL,(CBS_VD_CURRENT_Y)
	LD	DE,0000h
CBS_VD_OFFSET_ROWS:		EQU	$ - 2
	OR	A
	SBC	HL,DE
	OR	A
	SBC	HL,BC
	LD	(CBS_VD_SOURCE_Y),HL

	CALL	CBS_VD_CHOOSE_DEST_CHUNK
	LD	HL,(CBS_VD_WIDTH)
	LD	DE,0000h
CBS_VD_SOURCE_COLUMN:		EQU	$ - 2
	OR	A
	SBC	HL,DE
	LD	DE,(CBS_VD_CHUNK)
	CALL	CBS_VD_MIN_HL_DE
	LD	DE,0000h
CBS_VD_DISTANCE:		EQU	$ - 2
	CALL	CBS_VD_MIN_HL_DE
	LD	(CBS_VD_CHUNK),HL

	LD	A,H
	OR	A
	JR	NZ,CBS_VD_START_HMMM
	LD	A,L
	CP	CBS_VD_TINY_MATCH_MAX+1
	JR	NC,CBS_VD_START_HMMM

; Tiny non-overlapping fragments avoid VDP command setup. Read at most eight
; source bytes into RAM, reset the VRAM address, then write them to destination.
	CALL	CBS_VD_WAIT_IF_COMMAND
	LD	HL,0000h
CBS_VD_SOURCE_Y:		EQU	$ - 2
	LD	B,H			; selected VRAM page
	LD	H,00h			; Y within page
CBS_VD_TINY_SCREEN_LINE_SHIFT:
	LD	A,07h			; patched by CBS_INIT_SCREEN

CBS_VD_TINY_LINE_SHIFT:
	ADD	HL,HL
	DEC	A
	JR	NZ,CBS_VD_TINY_LINE_SHIFT
	LD	DE,(CBS_VD_START_X)
	ADD	HL,DE
	LD	DE,(CBS_VD_SOURCE_COLUMN)
	ADD	HL,DE

	PUSH	HL
	LD	A,H
	AND	C0h
	RLCA
	RLCA
	LD	C,A
	LD	A,B
CBS_VD_TINY_SCREEN_PAGE_SHIFT:
	LD	B,01h			; patched by CBS_INIT_SCREEN

CBS_VD_TINY_PAGE_SHIFT:
	ADD	A,A
	DJNZ	CBS_VD_TINY_PAGE_SHIFT
	OR	C
CBS_VD_PORT1_TINY_R14_VALUE:
	OUT	(99h),A
	LD	A,8Eh
CBS_VD_PORT1_TINY_R14_SELECT:
	OUT	(99h),A
	POP	HL
	LD	A,L
CBS_VD_PORT1_TINY_ADDRESS_LOW:
	OUT	(99h),A
	LD	A,H
	AND	3Fh
CBS_VD_PORT1_TINY_ADDRESS_HIGH:
	OUT	(99h),A

	LD	HL,CBS_VD_TINY_BUFFER
	LD	A,(CBS_VD_CHUNK)
	LD	B,A
CBS_VD_PORT0_TINY_READ:
	LD	C,98h
	INIR
	CALL	CBS_VD_SET_WRITE_ADDRESS
	LD	HL,CBS_VD_TINY_BUFFER
	LD	A,(CBS_VD_CHUNK)
	LD	B,A
CBS_VD_PORT0_TINY_WRITE:
	LD	C,98h
	OTIR
	JR	CBS_VD_MATCH_ADVANCE

; Larger fragments build R32-R43 directly in registers. C remains the VDP
; command port, eliminating both the writable command block and its OTIR.
CBS_VD_START_HMMM:
	CALL	CBS_VD_WAIT_IF_COMMAND

	LD	A,20h
CBS_VD_PORT1_COMMAND_INDEX:
	OUT	(99h),A
	LD	A,91h
CBS_VD_PORT1_INDIRECT_SELECT:
	OUT	(99h),A
CBS_VD_PORT3_HMMM:
	LD	C,9Bh

	LD	HL,(CBS_VD_START_X)
	LD	DE,(CBS_VD_SOURCE_COLUMN)
	ADD	HL,DE
CBS_VD_SCREEN_SHIFT_SX:
	LD	B,01h			; patched by CBS_INIT_SCREEN
	CALL	CBS_VD_SHIFT_HL
	OUT	(C),L			; R32 SX low
	OUT	(C),H			; R33 SX high

	LD	HL,(CBS_VD_SOURCE_Y)
	OUT	(C),L			; R34 SY low
	OUT	(C),H			; R35 SY high

	LD	HL,0000h
CBS_VD_START_X:			EQU	$ - 2
	LD	DE,(CBS_VD_COLUMN)
	ADD	HL,DE
CBS_VD_SCREEN_SHIFT_DX:
	LD	B,01h			; patched by CBS_INIT_SCREEN
	CALL	CBS_VD_SHIFT_HL
	OUT	(C),L			; R36 DX low
	OUT	(C),H			; R37 DX high

	LD	HL,(CBS_VD_CURRENT_Y)
	OUT	(C),L			; R38 DY low
	OUT	(C),H			; R39 DY high

	LD	HL,(CBS_VD_CHUNK)
CBS_VD_SCREEN_SHIFT_NX:
	LD	B,01h			; patched by CBS_INIT_SCREEN
	CALL	CBS_VD_SHIFT_HL
	LD	A,H
	AND	01h			; VDP encodes NX=512 as zero
	LD	H,A
	OUT	(C),L			; R40 NX low
	OUT	(C),H			; R41 NX high
	LD	A,01h
	LD	(CBS_VD_COMMAND_ACTIVE),A

	OUT	(C),A			; R42 NY low
	DEC	A
	OUT	(C),A			; R43 NY high
	OUT	(C),A			; R44 unused
	OUT	(C),A			; R45 forward
	LD	A,D0h
	OUT	(C),A			; R46 HMMM

CBS_VD_MATCH_ADVANCE:
	CALL	CBS_VD_ADVANCE
	CALL	CBS_VD_SUBTRACT_CHUNK
	JP	NZ,CBS_VD_COPY_MATCH_LOOP
	RET

; -----------------------------------------------------------------------------
; Choose min(token remaining, bytes left in destination row).
; -----------------------------------------------------------------------------
CBS_VD_CHOOSE_DEST_CHUNK:
	LD	HL,(CBS_VD_WIDTH)
	LD	DE,0000h
CBS_VD_COLUMN:			EQU	$ - 2
	OR	A
	SBC	HL,DE
	LD	DE,0000h
CBS_VD_REMAIN:			EQU	$ - 2
	CALL	CBS_VD_MIN_HL_DE
	LD	(CBS_VD_CHUNK),HL
	RET

; Return min(HL,DE) in HL.
CBS_VD_MIN_HL_DE:
	PUSH	HL
	OR	A
	SBC	HL,DE
	POP	HL
	RET	C
	RET	Z
	EX	DE,HL
	RET

CBS_VD_SUBTRACT_CHUNK:
	LD	HL,(CBS_VD_REMAIN)
	LD	DE,(CBS_VD_CHUNK)
	OR	A
	SBC	HL,DE
	LD	(CBS_VD_REMAIN),HL
	LD	A,H
	OR	L
	RET

; Advance the packed destination coordinate.
CBS_VD_ADVANCE:
	LD	HL,(CBS_VD_COLUMN)
	LD	DE,(CBS_VD_CHUNK)
	ADD	HL,DE
	LD	(CBS_VD_COLUMN),HL
	LD	DE,(CBS_VD_WIDTH)
	OR	A
	SBC	HL,DE
	RET	NZ

	LD	HL,0000h
	LD	(CBS_VD_COLUMN),HL
	LD	HL,0000h
CBS_VD_CURRENT_Y:		EQU	$ - 2
	INC	HL
	LD	(CBS_VD_CURRENT_Y),HL
	RET

; -----------------------------------------------------------------------------
; Start HMMV for the destination rectangle held in two immediate operands.
; R17 starts at R36, so only DX, DY, NX and NY are emitted before R44-R46.
; -----------------------------------------------------------------------------
CBS_VD_START_HMMV:
	LD	A,24h
CBS_VD_PORT1_FILL_COMMAND_INDEX:
	OUT	(99h),A
	LD	A,91h
CBS_VD_PORT1_FILL_INDIRECT_SELECT:
	OUT	(99h),A
CBS_VD_PORT3_HMMV:
	LD	C,9Bh

	LD	HL,(CBS_VD_START_X)
	LD	DE,(CBS_VD_COLUMN)
	ADD	HL,DE
CBS_VD_HMMV_SCREEN_SHIFT_DX:
	LD	B,01h			; patched by CBS_INIT_SCREEN
	CALL	CBS_VD_SHIFT_HL
	OUT	(C),L			; R36 DX low
	OUT	(C),H			; R37 DX high

	LD	HL,(CBS_VD_CURRENT_Y)
	OUT	(C),L			; R38 DY low
	OUT	(C),H			; R39 DY high

	LD	HL,0000h
CBS_VD_HMMV_NX:			EQU	$ - 2
CBS_VD_HMMV_SCREEN_SHIFT_NX:
	LD	B,01h			; patched by CBS_INIT_SCREEN
	CALL	CBS_VD_SHIFT_HL
	LD	A,H
	AND	01h			; VDP encodes NX=512 as zero
	LD	H,A
	OUT	(C),L			; R40 NX low
	OUT	(C),H			; R41 NX high

	LD	HL,0001h
CBS_VD_HMMV_NY:			EQU	$ - 2
	OUT	(C),L			; R42 NY low
	OUT	(C),H			; R43 NY high

	LD	A,(CBS_VD_FILL_COLOR_LOAD+1)
	OUT	(C),A			; R44 packed fill byte
	XOR	A
	OUT	(C),A			; R45 forward
	LD	A,C0h
	OUT	(C),A			; R46 HMMV

	LD	A,01h
	LD	(CBS_VD_COMMAND_ACTIVE),A
	RET

CBS_VD_SHIFT_HL:
	LD	A,B
	OR	A
	RET	Z
	ADD	HL,HL
	DEC	B
	JR	CBS_VD_SHIFT_HL

; -----------------------------------------------------------------------------
; CPU VRAM write address for the current destination packed byte coordinate.
; -----------------------------------------------------------------------------
CBS_VD_SET_WRITE_ADDRESS:
	LD	HL,(CBS_VD_CURRENT_Y)
	LD	B,H			; selected VRAM page
	LD	H,00h			; Y within page
CBS_VD_SCREEN_LINE_SHIFT:
	LD	A,07h			; patched by CBS_INIT_SCREEN

CBS_VD_ADDRESS_LINE_SHIFT:
	ADD	HL,HL
	DEC	A
	JR	NZ,CBS_VD_ADDRESS_LINE_SHIFT
	LD	DE,(CBS_VD_START_X)
	ADD	HL,DE
	LD	DE,(CBS_VD_COLUMN)
	ADD	HL,DE

	PUSH	HL
	LD	A,H
	AND	C0h
	RLCA
	RLCA
	LD	C,A
	LD	A,B
CBS_VD_SCREEN_PAGE_SHIFT:
	LD	B,01h			; patched by CBS_INIT_SCREEN

CBS_VD_ADDRESS_PAGE_SHIFT:
	ADD	A,A
	DJNZ	CBS_VD_ADDRESS_PAGE_SHIFT
	OR	C
CBS_VD_PORT1_R14_VALUE:
	OUT	(99h),A
	LD	A,8Eh
CBS_VD_PORT1_R14_SELECT:
	OUT	(99h),A			; write R14
	POP	HL
	LD	A,L
CBS_VD_PORT1_ADDRESS_LOW:
	OUT	(99h),A
	LD	A,H
	AND	3Fh
	OR	40h
CBS_VD_PORT1_ADDRESS_HIGH:
	OUT	(99h),A			; CPU VRAM write
	RET

; -----------------------------------------------------------------------------
; Interlaced Elias readers shared by v5.3 and v5.4.
; -----------------------------------------------------------------------------
CBS_VD_ELIAS:
	LD	BC,0001h			; VDP writers do not leave BC at zero
	JR	CBS_VD_ELIAS_LOOP

CBS_VD_GET_BIT:
	ADD	A,A
	RET	NZ
	LD	A,(HL)
	INC	HL
	RLA
	RET

CBS_VD_ELIAS_LOOP:
	ADD	A,A
	JR	NZ,CBS_VD_ELIAS_SKIP
	LD	A,(HL)
	INC	HL
	RLA

CBS_VD_ELIAS_SKIP:
	RET	C

CBS_VD_ELIAS_BACKTRACK:
	ADD	A,A
	JR	NZ,CBS_VD_ELIAS_BACKTRACK_READY
	LD	A,(HL)
	INC	HL
	RLA
CBS_VD_ELIAS_BACKTRACK_READY:
	RL	C
	RL	B
	JR	CBS_VD_ELIAS_LOOP

; A = Screen mode 5-8. The trusted caller supplies a valid mode. Each table row
; is pixel shift, VRAM line shift and page-bank shift respectively.
CBS_VD_INIT_SCREEN:
	SUB	5
	LD	E,A
	ADD	A,A
	ADD	A,E
	LD	E,A
	LD	D,00h
	LD	HL,CBS_VD_SCREEN_TABLE
	ADD	HL,DE

	LD	A,(HL)
	INC	HL
	LD	(CBS_VD_SCREEN_SHIFT_DX+1),A
	LD	(CBS_VD_SCREEN_SHIFT_NX+1),A
	LD	(CBS_VD_SCREEN_SHIFT_SX+1),A
	LD	(CBS_VD_HMMV_SCREEN_SHIFT_DX+1),A
	LD	(CBS_VD_HMMV_SCREEN_SHIFT_NX+1),A

	LD	A,(HL)
	INC	HL
	LD	(CBS_VD_SCREEN_LINE_SHIFT+1),A
	LD	(CBS_VD_OFFSET1_READ_SCREEN_LINE_SHIFT+1),A
	LD	(CBS_VD_TINY_SCREEN_LINE_SHIFT+1),A
	PUSH	HL
	LD	HL,0080h
	CP	08h
	JR	NZ,CBS_VD_LINEAR_WIDTH_READY
	ADD	HL,HL
CBS_VD_LINEAR_WIDTH_READY:
	LD	(CBS_VD_LINEAR_WIDTH),HL
	POP	HL
	LD	A,(HL)
	LD	(CBS_VD_SCREEN_PAGE_SHIFT+1),A
	LD	(CBS_VD_OFFSET1_READ_SCREEN_PAGE_SHIFT+1),A
	LD	(CBS_VD_TINY_SCREEN_PAGE_SHIFT+1),A
	RET

CBS_VD_SCREEN_TABLE:
	DB	01h,07h,01h			; Screen 5
	DB	02h,07h,01h			; Screen 6
	DB	01h,08h,02h			; Screen 7
	DB	00h,08h,02h			; Screen 8

; -----------------------------------------------------------------------------
; Eight-byte scratch history for CPU-assisted tiny VRAM matches. Keeping this
; separate leaves the one-time machine initializer intact across every image.
; -----------------------------------------------------------------------------
CBS_VD_TINY_BUFFER:
	DS	CBS_VD_TINY_MATCH_MAX,00h

; -----------------------------------------------------------------------------
; One-time machine initializer. VDP.DW at 0007h supplies the write base port;
; VDP.DR at 0006h independently supplies the read base port. Call CBS_INIT_MSX
; once before the first decrunch. It remains intact after every image.
; -----------------------------------------------------------------------------
CBS_VD_INIT_MSX:
	LD	A,(0007h)
	LD	(CBS_VD_PORT0_LITERAL+1),A
	LD	(CBS_VD_PORT0_SHORT_FILL+1),A
	LD	(CBS_VD_PORT0_TINY_WRITE+1),A
	INC	A
	LD	(CBS_VD_PORT1_COMMAND_INDEX+1),A
	LD	(CBS_VD_PORT1_INDIRECT_SELECT+1),A
	LD	(CBS_VD_PORT1_R14_VALUE+1),A
	LD	(CBS_VD_PORT1_R14_SELECT+1),A
	LD	(CBS_VD_PORT1_ADDRESS_LOW+1),A
	LD	(CBS_VD_PORT1_ADDRESS_HIGH+1),A
	LD	(CBS_VD_PORT1_FILL_COMMAND_INDEX+1),A
	LD	(CBS_VD_PORT1_FILL_INDIRECT_SELECT+1),A
	LD	(CBS_VD_PORT1_OFFSET1_R14_VALUE+1),A
	LD	(CBS_VD_PORT1_OFFSET1_R14_SELECT+1),A
	LD	(CBS_VD_PORT1_OFFSET1_ADDRESS_LOW+1),A
	LD	(CBS_VD_PORT1_OFFSET1_ADDRESS_HIGH+1),A
	LD	(CBS_VD_PORT1_TINY_R14_VALUE+1),A
	LD	(CBS_VD_PORT1_TINY_R14_SELECT+1),A
	LD	(CBS_VD_PORT1_TINY_ADDRESS_LOW+1),A
	LD	(CBS_VD_PORT1_TINY_ADDRESS_HIGH+1),A
	LD	(CBS_VD_PORT1_STATUS_INDEX+1),A
	LD	(CBS_VD_PORT1_STATUS_SELECT+1),A
	LD	(CBS_VD_PORT1_STATUS_ZERO+1),A
	LD	(CBS_VD_PORT1_STATUS_RESTORE+1),A
	INC	A
	INC	A
	LD	(CBS_VD_PORT3_HMMM+1),A
	LD	(CBS_VD_PORT3_HMMV+1),A

	LD	A,(0006h)
	LD	(CBS_VD_PORT0_OFFSET1_READ+1),A
	LD	(CBS_VD_PORT0_TINY_READ+1),A
	INC	A
	LD	(CBS_VD_PORT1_STATUS_READ+1),A
	RET
