diff --git a/gbadisplay/gbadisplay.go b/gbadisplay/gbadisplay.go new file mode 100644 index 000000000..fea99cdd0 --- /dev/null +++ b/gbadisplay/gbadisplay.go @@ -0,0 +1,85 @@ +// Package gbadisplay implements a simple driver for the GameBoy Advance +// display. +package gbadisplay + +import ( + "device/gba" + "errors" + "image/color" + "runtime/volatile" + "unsafe" + + "tinygo.org/x/drivers/pixel" +) + +// Image buffer type used by the GameBoy Advance. +type Image = pixel.Image[pixel.RGB555] + +const ( + displayWidth = 240 + displayHeight = 160 +) + +var ( + errOutOfBounds = errors.New("rectangle coordinates outside display area") +) + +type Device struct{} + +// New returns a new GameBoy Advance display object. +func New() Device { + return Device{} +} + +var displayFrameBuffer = (*[160 * 240]volatile.Register16)(unsafe.Pointer(uintptr(gba.MEM_VRAM))) + +type Config struct { + // TODO: add more display modes here. +} + +// Configure the display as a regular 15bpp framebuffer. +func (d Device) Configure(config Config) { + // Use video mode 3 (in BG2, a 16bpp bitmap in VRAM) and Enable BG2. + gba.DISP.DISPCNT.Set(gba.DISPCNT_BGMODE_3<= displayWidth || y > displayHeight { + // Out of bounds, so ignore. + return + } + val := pixel.NewColor[pixel.RGB555](c.R, c.G, c.B) + displayFrameBuffer[(int(y))*240+int(x)].Set(uint16(val)) +} + +// DrawBitmap updates the rectangle at (x, y) to the image stored in buf. +func (d Device) DrawBitmap(x, y int16, buf Image) error { + width, height := buf.Size() + if x < 0 || y < 0 || int(x)+width > displayWidth || int(y)+height > displayHeight { + return errOutOfBounds + } + + // TODO: try to do a 4-byte memcpy if possible. That should significantly + // speed up the copying of this image. + for bufY := 0; bufY < int(height); bufY++ { + for bufX := 0; bufX < int(width); bufX++ { + val := buf.Get(bufX, bufY) + displayFrameBuffer[(int(y)+bufY)*240+int(x)+bufX].Set(uint16(val)) + } + } + + return nil +} diff --git a/pixel/pixel.go b/pixel/pixel.go index 940fb1c5c..17db4f33e 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -16,7 +16,7 @@ import ( // particular display. Each pixel is at least 1 byte in size. // The color format is sRGB (or close to it) in all cases. type Color interface { - RGB888 | RGB565BE | RGB444BE + RGB888 | RGB565BE | RGB555 | RGB444BE BaseColor } @@ -46,6 +46,8 @@ func NewColor[T Color](r, g, b uint8) T { return any(NewRGB888(r, g, b)).(T) case RGB565BE: return any(NewRGB565BE(r, g, b)).(T) + case RGB555: + return any(NewRGB555(r, g, b)).(T) case RGB444BE: return any(NewRGB444BE(r, g, b)).(T) default: @@ -140,6 +142,35 @@ func (c RGB565BE) RGBA() color.RGBA { return color } +// Color format used on the GameBoy Advance among others. +// +// Colors are stored as native endian values, with bits 0bbbbbgg_gggrrrrr (red +// is least significant, blue is most significant). +type RGB555 uint16 + +func NewRGB555(r, g, b uint8) RGB555 { + return RGB555(r)>>3 | (RGB555(g)>>3)<<5 | (RGB555(b)>>3)<<10 +} + +func (c RGB555) BitsPerPixel() int { + // 15 bits per pixel, but there are 16 bits when stored + return 16 +} + +func (c RGB555) RGBA() color.RGBA { + color := color.RGBA{ + R: uint8(c>>10) << 3, + G: uint8(c>>5) << 3, + B: uint8(c) << 3, + A: 255, + } + // Correct color rounding, so that 0xff roundtrips back to 0xff. + color.R |= color.R >> 5 + color.G |= color.G >> 5 + color.B |= color.B >> 5 + return color +} + // Color format that is supported by the ST7789 for example. // It may be a bit faster to use than RGB565BE on very slow SPI buses. //