优化GameBoy Z80中的位操作算法

13

这不是一道作业题,而是我正在开发的一个游戏。

我有两个16位RGB颜色,并希望根据另外六个4位数量来改变它们的六个通道。虽然算法简单但很繁琐,我正在寻找一种优化方法,以便一次处理更多有用的工作。

这是高级概述:

  • hl指向四个颜色字节:[hl] = %gggrrrrr[hl+1] = %0bbbbbgg[hl+2] = %GGGRRRRR[hl+3] = %0BBBBBGG。(这是两种颜色,即rgbRGB
  • bc指向三个增量字节:[bc] = %hhhhaaaa[bc+1] = %ddddssss[bc+2] = %ppppqqqq。(这是六个增量值:hadspq
  • 因此有六个5位颜色通道值和六个4位增量值。我想将每个颜色通道C与一个增量值D配对,并按如下方式变化CC' = C + (D & %11) − ((D & %1100) >> 2),但保持C在其5位范围内[0, 31]。我并不关心它们是如何配对的:任何方便的一对一配对都可以。如果C + ((D & %1100) >> 2) − (D & %11) 可以更优雅地解决问题,那么我也可以接受。

如果我将一个颜色通道C隔离到寄存器d中,将一个增量值D隔离到寄存器e中,那么这个例程将为该对执行变化:

VaryColorChannelByDV:
; d = color, e = DV
; a <- d + (e & %11) - (e >> 2), clamped to [0, 31]
    ld a, e
    and %11   ; a <- (e & %11)
    add d   ; a <- d + (e & %11)
    srl e
    srl e   ; e <- e >> 2
    sub e   ; a <- d + (e & %11) - (e >> 2)
    jr c, .zero   ; a < 0, clamp to 0
    cp 32
    ret c   ; 0 <= a < 32
    ld a, 31   ; a >= 32, clamp to 31
    ret
.zero
    xor a
    ret

目前我有一个通用程序,可以将任何DV应用于任何颜色通道;然后有三个程序将红、绿或蓝通道隔离出来,并将给定的DV应用于它们;最后是一个主程序,选出六个DV并调用相应的通道修改程序。这样已经“足够好了”,但我确定还有改进的空间。执行速度似乎不是问题,但我想减少代码大小(当然,删除冗余指令也会稍微提高速度)。是否有任何汇编位运算技巧可以帮助?

以下是完整代码:

GetColorChannelVariedByDV:
; d = color, e = DV
; a <- d + (e & %11) - (e & %1100 >> 2), clamped to [0, 31]
    ld a, e
    and %11
    add d
    srl e
    srl e
    sub e
    jr c, .zero
    cp 32
    ret c
    ld a, 31
    ret
.zero
    xor a
    ret

VaryRedByDV:
;;; e = DV
;;; [hl+0] = gggr:rrrr
;;; [hl+1] = 0bbb:bbgg
; store red in d
    ld a, [hl]
    and %00011111
    ld d, a
; vary d according to e
    call GetColorChannelVariedByDV
; store a back in red
    ld d, a
    ld a, [hl]
    and %11100000
    or d
    ld [hl], a
    ret

VaryGreenByDV:
;;; e = DV
;;; [hl+0] = gggr:rrrr
;;; [hl+1] = 0bbb:bbgg
; store green in d
    ld a, [hli]
    and %11100000
    srl a
    swap a
    ld d, a ; d = 00000ggg
    ld a, [hld]
    and %00000011
    swap a
    srl a
    or d
    ld d, a
; vary d according to e
    call GetColorChannelVariedByDV
; store a back in green
    sla a
    swap a
    ld d, a
    and %11100000
    ld e, a
    ld a, d
    and %00000011
    ld d, a
    ld a, [hl]
    and %00011111
    or e
    ld [hli], a
    ld a, [hl]
    and %11111100
    or d
    ld [hld], a
    ret

VaryBlueByDV:
;;; e = DV
;;; [hl+0] = gggr:rrrr
;;; [hl+1] = 0bbb:bbgg
; store blue in d
    inc hl
    ld a, [hl]
    and %01111100
    srl a
    srl a
    ld d, a
; vary d according to e
    call GetColorChannelVariedByDV
; store a back in blue
    ld d, a
    sla d
    sla d
    ld a, [hl]
    and %10000011
    or d
    ld [hl], a
    dec hl
    ret

VaryColorsByDVs::
; hl = colors
; [hl+0] = gggr:rrrr
; [hl+1] = 0bbb:bbgg
; [hl+2] = GGGR:RRRR
; [hl+3] = 0BBB:BBGG
; bc = DVs
; [bc+0] = hhhh:aaaa
; [bc+1] = dddd:ssss
; [bc+2] = pppp:qqqq

;;; LiteRed ~ hDV, aka, rrrrr ~ hhhh
; store hDV in e
    ld a, [bc]
    swap a
    and %1111
    ld e, a
; vary LiteRed by e
    call VaryRedByDV

;;; LiteGrn ~ aDV, aka, ggggg ~ aaaa
; store aDV in e
    ld a, [bc]
    and %1111
    ld e, a
; vary LiteGrn by e
    call VaryGreenByDV

;;; move from h/a DV to d/s DV
    inc bc

;;; LiteBlu ~ dDV, aka, bbbbb ~ dddd
; store dDV in e
    ld a, [bc]
    swap a
    and %1111
    ld e, a
; vary LiteBlu by e
    call VaryBlueByDV

;;; Move from Lite color to Dark color
    inc hl
    inc hl

;;; DarkRed ~ sDV, aka, RRRRR ~ ssss
; store sDV in e
    ld a, [bc]
    and %1111
    ld e, a
; vary DarkRed by e
    call VaryRedByDV

;;; move from d/s DV to p/q DV
    inc bc

;;; DarkGrn ~ pDV, aka, GGGGG ~ pppp
; store pDV in e
    ld a, [bc]
    swap a
    and %1111
    ld e, a
; vary DarkGrn by e
    call VaryGreenByDV

;;; DarkBlu ~ qDV, aka, BBBBB ~ qqqq
; store qDV in e
    ld a, [bc]
    and %1111
    ld e, a
; vary DarkBlu by e
    call VaryBlueByDV

    ret

在操作之前,为什么不将delta值(二位)相加?我的意思是,当D例如为%1001时,您添加1并减去2,这导致减1。您能否将delta表示为单个3位有符号值? - Roman Hocke
或者,您可以为每个4位增量组合创建16字节长的查找表,这样您就不需要每次添加和减去增量的两个位。 - Roman Hocke
看起来是一个有趣的、构思良好的问题,所以我点了个赞!不幸的是,我不确定在 Stack Overflow 上有多少 GameBoy 汇编语言专家,所以如果你在一两天内没有得到你想要的答案,你可以考虑将你代码中重要的部分翻译成伪代码,这样我们所有人都可以参与进来,即使我们不知道你的母语是什么。它看起来确实有一些冗余操作,但我对语法还不够熟悉,无法自信地告诉你。 - Cody Gray
您是否意味着将5位(0-31)输入颜色分量饱和到4位(0-15)?如果是这样,那么输入颜色也在该范围内吗? - jacobly
我没有混淆5位颜色和4位增量。我已经更新了帖子。 - Remy
2个回答

9
我现在能想到的最小值是57字节:
VaryColorsByDVs::
; hl = colors
; [hl+0] = gggr:rrrr
; [hl+1] = 0bbb:bbgg
; [hl+2] = GGGR:RRRR
; [hl+3] = 0BBB:BBGG
; bc = DVs
; [bc+0] = hhhh:aaaa
; [bc+1] = dddd:ssss
; [bc+2] = pppp:qqqq
    ld a, 2 ; -floor($100/3)*6 mod $100
.next:
    sla [hl]
    inc hl
    rl [hl]
.loop:
    push af
    rrca
    ld a, [bc]
    jr nc, .skip
    swap a
    inc bc
.skip:
    rlca
    ld d, a
    and %00011000
    ld e, a
    ld a, d
    rlca
    rlca
    and %00011000
    add a, [hl]
    jr nc, .noOverflow
    or %11111000
.noOverflow:
    sub e
    jr nc, .noUnderflow
    and %00000111
.noUnderflow:
    dec hl
    ld de, 5
.rotate:
    add a, a
    rl [hl]
    adc a, d
    dec e
    jr nz, .rotate
    inc hl
    ld [hl], a
    pop af
    add a, 85 ; floor($100/3)
    jr nc, .loop
    ret z
    inc hl
    jr .next

修复Ped7g的注释只需要4个字节,总共61个字节:

VaryColorsByDVs::
; hl = colors
; [hl+0] = gggr:rrrr
; [hl+1] = 0bbb:bbgg
; [hl+2] = GGGR:RRRR
; [hl+3] = 0BBB:BBGG
; bc = DVs
; [bc+0] = hhhh:aaaa
; [bc+1] = dddd:ssss
; [bc+2] = pppp:qqqq
    ld a, 2 ; -floor($100/3)*6 mod $100
.next:
    sla [hl]
    inc hl
    rl [hl]
.loop:
    push af
    rrca
    ld a, [bc]
    jr nc, .skip
    swap a
    inc bc
.skip:
    ld d, a
    and %00001100
    ld e, a
    ld a, d
    rlca
    rlca
    and %00001100
    sub e
    add a, a
    jr nc, .positive
.negative:
    add a, [hl]
    jr c, .continue
    and %00000111
    db $38 ; jr c,
.positive:
    add a, [hl]
    jr nc, .continue
    or %11111000
.continue:
    dec hl
    ld de, 5
.rotate:
    add a, a
    rl [hl]
    adc a, d
    dec e
    jr nz, .rotate
    inc hl
    ld [hl], a
    pop af
    add a, 85 ; floor($100/3)
    jr nc, .loop
    ret z
    inc hl
    jr .next

这是在“-(DV>>2)”之前夹紧最大颜色值,因此颜色31与DV 0x1111将以28结束。(只是为了告诉OP,让他决定是否可以接受)。 - Ped7g
这绝对是为大小进行了优化!谢谢!如果您能让它更小,我会印象深刻的,但那将是代码高尔夫,不是真正必要的。您是否以相同的方式配对通道和 DV?如果没有也没关系。 - Remy
另外,“db $38”是否使用随后的指令值($86,对吧?)作为相对跳转量?非常棘手。;)虽然这似乎是太大的跳跃距离,所以我认为我对此有些误解。 - Remy
1
配对是bgrBGR <-> ahsdqp。 "db $38"技巧看起来像jr c,对于CPU来说是垃圾,但是前一个指令实际上清除了进位标志,因此它总是跳过以下指令,但只花费1个字节。同样的事情也可以用"db $16 ; ld d,"来完成,但我更喜欢条件jr操作码,因为它们不会破坏任何寄存器。 - jacobly
@Remy 你说的“would be code golf”是什么意思?这段代码已经被压缩到极致了。 - Ped7g

3
嗯......您应该提供更多有关数据来源的信息,如果可以进一步预处理这些数据,因为那个 +(d&3)-(d>>2) 看起来不太好,如果可能的话,我会尽量避免。实际上整个 5:5:5 RGB 的东西可能对于 Z80 来说有点超纲了,但是如果您知道它适合您,请继续(我是从我的 ZX Spectrum 经验中谈论的,在那里 3.5MHz 几乎不足以操作 1 位 B&W 像素)。
但是目前你已经拥有的东西,可以通过删除两个 ld 指令来进行简化。
VaryColorChannelByDV:
    ...
    add d
;    ld d, a   ; d <- d + (e & %11)
    srl e
    srl e
;    ld a, d   ;### A didn't change, still contains C + DV&3
    sub e   ; a <- d + (e & %11) - (e & %1100 >> 2)
    ...

如果内存不短缺,您可以创建256B查找表来夹紧值。例如,您可以在hb中保留表的高地址字节,然后将结果加载到lc中,并通过ld a,(hl / bc)夹紧。这只需要4+7个t,而不是那些jr / cp / ret / ...。实际上,您只需要从256个值中选择一些值,从-3到34(0..34和253..255),如果我没有计算错误的话(0 + 0 - 3是最小值,31 + 3 - 0是最大结果)。因此,您仍然可以使用“页面内”的地址35..252的字节来存储其他数据或代码。
稍后我会尝试整体查看它,以避免某些组件通用性问题(如果可能的话),但恐怕更好的输入数据格式可能会给您带来更大的提升,或者了解您的总体目标和所有约束条件(例如RGB中的最高位始终为0并且必须为0,或者可以作为结果随机,作为输入为0等等...每个细节通常都可以导致另一个删除指令,这通常值得Z80的4-11 t)。

我正在编辑的游戏中已经使用了5:5:5 RGB格式。我认为没有任何预处理是可能的:颜色值是从表格中获取的(有时也需要不变的颜色),而增量值是该例程之外的有意义的数据。我所要做的就是在hl和bc中获取它们的指针,然后调用该例程。速度不是问题,空间才是;减少指令很好,但我认为一个256字节的LUT在空间上浪费比在时间上节省更多。 - Remy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接