Livt packages

Eccelerators.Matrix

Version 1.0.1

Reusable RGB matrix drawing, color conversion, animation pacing, and HUB75 display components for Livt.

Published files (48)

src/Hub75/Hub75Display.lvt

namespace Eccelerators.Matrix.Hub75

using Eccelerators.Matrix

/**
 * Own a logical canvas spanning one or more identical chained HUB75 panels.
 * Drawing uses canvas coordinates; this facade maps panel tiles into physical
 * serial order before writing double-buffered scan storage.
 */
component Hub75Display<PANELS_X: int = 1, PANELS_Y: int = 1,
	PANEL_WIDTH: int = 64, PANEL_HEIGHT: int = 64,
	CHAIN_ORDER: Hub75ChainOrder = Hub75ChainOrder.RowMajor,
	COLOR_BITS: int = 6, PHASE_TIME: Time = 100ns,
	STARTUP_TIME: Time = 50ms, ROW_HOLD_TIME: Time = 150us> : IMatrixDisplay, IMatrixLayout
{
	static assert PANELS_X >= 1 && PANELS_X <= 16, "Panel columns must be 1..16"
	static assert PANELS_Y >= 1 && PANELS_Y <= 16, "Panel rows must be 1..16"
	static assert PANELS_X * PANELS_Y <= 16, "A HUB75 chain supports at most 16 panels"
	static assert PANEL_WIDTH >= 2 && PANEL_WIDTH <= 256, "Panel width must be 2..256"
	static assert PANEL_HEIGHT >= 4 && PANEL_HEIGHT <= 64 && (PANEL_HEIGHT & (PANEL_HEIGHT - 1)) == 0, "Panel height must be a power of two in 4..64"
	static assert PANEL_WIDTH * PANELS_X * PANELS_Y <= 1024, "A scan row supports at most 1024 shifted pixels"
	static assert PHASE_TIME > 0ns, "Scan phase must be positive"
	static assert STARTUP_TIME > 0ns, "Startup time must be positive"
	static assert ROW_HOLD_TIME > 0ns, "Row hold budget must be positive"
	static assert COLOR_BITS == 4 || COLOR_BITS == 6, "Display depth must be RGB444 or RGB666"

	// Public logical geometry and physical channel depth.
	public const PANEL_COUNT: int = PANELS_X * PANELS_Y
	public const WIDTH: int = PANELS_X * PANEL_WIDTH
	public const HEIGHT: int = PANELS_Y * PANEL_HEIGHT
	public const BITS_PER_CHANNEL: int = COLOR_BITS
	const SHIFT_COLUMNS: int = PANEL_COUNT * PANEL_WIDTH
	const ROW_BITS: int = Bits.RequiredFor(PANEL_HEIGHT / 2 - 1)
	const PIXEL_BITS: int = 3 * COLOR_BITS

	// Owned scan engine and physical-order image storage.
	core: Hub75Controller<SHIFT_COLUMNS, ROW_BITS, PHASE_TIME, STARTUP_TIME, 10us, COLOR_BITS, ROW_HOLD_TIME>
	framebuffer: Hub75Framebuffer<SHIFT_COLUMNS, ROW_BITS, COLOR_BITS>

	// Optional logical-panel RGB gains; false entries use neutral gain.
	panelCalibrated: bool[PANEL_COUNT]
	panelRedGain: int[PANEL_COUNT]
	panelGreenGain: int[PANEL_COUNT]
	panelBlueGain: int[PANEL_COUNT]

	/**
	 * Bind the shared HUB75 chain pins and construct scan storage. PHASE_TIME
	 * must span at least five inherited context clocks.
	 */
	new(panelClock: out logic, latch: out logic, outputEnableN: out logic,
		row: out logic[ROW_BITS], rgbUpper: out logic[3], rgbLower: out logic[3])
	{
		core = new Hub75Controller<SHIFT_COLUMNS, ROW_BITS, PHASE_TIME, STARTUP_TIME, 10us, COLOR_BITS, ROW_HOLD_TIME>()
		framebuffer = new Hub75Framebuffer<SHIFT_COLUMNS, ROW_BITS, COLOR_BITS>()
		panelClock = core.panelClock
		latch = core.latch
		outputEnableN = core.outputEnableN
		row = core.row
		rgbUpper = core.rgbUpper
		rgbLower = core.rgbLower
	}

	override fn GetWidth() int
	{
		return WIDTH
	}

	override fn GetHeight() int
	{
		return HEIGHT
	}

	override fn GetPanelColumns() int
	{
		return PANELS_X
	}

	override fn GetPanelRows() int
	{
		return PANELS_Y
	}

	override fn GetPanelWidth() int
	{
		return PANEL_WIDTH
	}

	override fn GetPanelHeight() int
	{
		return PANEL_HEIGHT
	}

	fn ApplyGain[](channel: int, gain: int) int
	{
		var adjusted = (channel * gain + 128) >> 8

		if (adjusted > 255) {
			return 255
		}

		return adjusted
	}

	fn Encode[](rgb: logic[24], panel: int) logic[PIXEL_BITS]
	{
		var red = (((rgb as uint) >> 16) & 255) as int
		var green = (((rgb as uint) >> 8) & 255) as int
		var blue = ((rgb as uint) & 255) as int

		if (panelCalibrated[panel]) {
			red = ApplyGain(red, panelRedGain[panel])
			green = ApplyGain(green, panelGreenGain[panel])
			blue = ApplyGain(blue, panelBlueGain[panel])
		}

		return ((((red as uint) >> (8 - COLOR_BITS)) << (2 * COLOR_BITS)) +
			(((green as uint) >> (8 - COLOR_BITS)) << COLOR_BITS) +
			((blue as uint) >> (8 - COLOR_BITS))) as logic[PIXEL_BITS]
	}

	/**
	 * Set per-panel RGB gains in 8.8 fixed point. 256 is neutral, 128 is 50
	 * percent and 0 disables a channel. Return false for an invalid tile or gain.
	 */
	public fn SetPanelGain(panelX: int, panelY: int, red: int, green: int, blue: int) bool
	{
		if (panelX < 0 || panelX >= PANELS_X || panelY < 0 || panelY >= PANELS_Y
			|| red < 0 || red > 512 || green < 0 || green > 512 || blue < 0 || blue > 512) {
			return false
		}

		var panel = panelY * PANELS_X + panelX
		panelRedGain[panel] = red
		panelGreenGain[panel] = green
		panelBlueGain[panel] = blue
		panelCalibrated[panel] = true
		return true
	}

	fn GetChainPosition[](panelX: int, panelY: int) int
	{
		var position: int

		static if (CHAIN_ORDER == Hub75ChainOrder.Serpentine || CHAIN_ORDER == Hub75ChainOrder.SerpentineReverse) {
			if ((panelY & 1) == 0) {
				position = panelY * PANELS_X + panelX
			} else {
				position = panelY * PANELS_X + (PANELS_X - 1 - panelX)
			}
		} else {
			position = panelY * PANELS_X + panelX
		}

		static if (CHAIN_ORDER == Hub75ChainOrder.RowMajorReverse || CHAIN_ORDER == Hub75ChainOrder.SerpentineReverse) {
			position = PANEL_COUNT - 1 - position
		}

		return position
	}

	fn ClearPanel(panelX: int, panelY: int, rgb: logic[24])
	{
		var panel = panelY * PANELS_X + panelX
		var chainPosition = GetChainPosition(panelX, panelY)
		var encoded = Encode(rgb, panel)

		for (var localY = 0; localY < PANEL_HEIGHT; localY++) {
			for (var localX = 0; localX < PANEL_WIDTH; localX++) {
				framebuffer.SetPixel(chainPosition * PANEL_WIDTH + localX, localY, encoded)
			}
		}
	}

	override fn Clear(rgb: logic[24])
	{
		framebuffer.Clear(Encode(rgb, 0))

		for (var panelY = 0; panelY < PANELS_Y; panelY++) {
			for (var panelX = 0; panelX < PANELS_X; panelX++) {
				var panel = panelY * PANELS_X + panelX

				if (panel > 0 && (panelCalibrated[0] || panelCalibrated[panel])) {
					ClearPanel(panelX, panelY, rgb)
				}
			}
		}
	}

	/**
	 * Map one logical canvas pixel into physical serial scan order. Apply the
	 * selected logical panel's gain and return false outside the canvas.
	 */
	override fn SetPixel(x: int, y: int, rgb: logic[24]) bool
	{
		if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) {
			return false
		}

		var panelX = x / PANEL_WIDTH
		var panelY = y / PANEL_HEIGHT
		var localX = x - panelX * PANEL_WIDTH
		var localY = y - panelY * PANEL_HEIGHT
		var physicalX = GetChainPosition(panelX, panelY) * PANEL_WIDTH + localX
		var panel = panelY * PANELS_X + panelX
		return framebuffer.SetPixel(physicalX, localY, Encode(rgb, panel))
	}

	/** Draw a borrowed bitmap at an offset, clipping it to the logical canvas. */
	public fn DrawBitmap(bitmap: IMatrixBitmap, left: int, top: int)
	{
		var width = bitmap.GetWidth()
		var height = bitmap.GetHeight()

		if (width <= 0 || height <= 0 || left >= WIDTH || top >= HEIGHT || left <= -width || top <= -height) {
			return
		}

		for (var y = 0; y < HEIGHT; y++) {
			for (var x = 0; x < WIDTH; x++) {
				if (x >= left && y >= top) {
					if (left > x - width && top > y - height) {
						SetPixel(x, y, bitmap.GetPixel(x - left, y - top))
					}
				}
			}
		}
	}

	override fn Present() bool
	{
		return framebuffer.Present()
	}

	/** Clear to black, draw a borrowed bitmap at the origin and present it. */
	public fn ShowBitmap(bitmap: IMatrixBitmap) bool
	{
		Clear(0 as logic[24])
		DrawBitmap(bitmap, 0, 0)
		return Present()
	}

	/** Connect scan requests, registered pixels and the frame-boundary handshake. */
	process Connect[]()
	{
		core.pixelUpper = framebuffer.rgbUpper
		core.pixelLower = framebuffer.rgbLower
		framebuffer.readColumn = core.readColumn
		framebuffer.readRow = core.readRow
		framebuffer.frameBoundary = core.frameBoundary
	}
}