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/Hub75Framebuffer.lvt

namespace Eccelerators.Matrix.Hub75

using Eccelerators.Matrix

/**
 * Own two packed RGB image banks and expose a synchronous scan source plus a
 * blocking drawing API. Use one scheduled drawing client in the same context;
 * Clear must complete before each Present. Reset invalidates both images and
 * outputs black without clearing block RAM. Never overlap drawing calls.
 */
component Hub75Framebuffer<SHIFT_COLUMNS: int = 64, ROW_BITS: int = 5, COLOR_BITS: int = 1>
{
	static assert SHIFT_COLUMNS >= 2 && SHIFT_COLUMNS <= 1024, "Shift columns must be 2..1024"
	static assert ROW_BITS > 0 && ROW_BITS <= 5, "Row bits must be 1..5"
	static assert COLOR_BITS >= 1 && COLOR_BITS <= 6, "Color bits must be 1..6"

	// Public pixel-coordinate bounds.
	const PIXEL_BITS: int = 3 * COLOR_BITS
	const COLUMN_BITS: int = Bits.RequiredFor(SHIFT_COLUMNS - 1)
	public const WIDTH: int = SHIFT_COLUMNS
	public const HEIGHT: int = 2 << ROW_BITS

	// Storage geometry: one lane contains two image banks.
	const LANE_SIZE: int = SHIFT_COLUMNS * (1 << ROW_BITS)
	const CAPACITY: int = 2 * LANE_SIZE

	// Upper-lane image banks; contents remain unspecified until Clear.
	@Memory(Style = MemoryStyle.Block)
	@UninitializedStorage
	upperMemory: logic[CAPACITY, PIXEL_BITS]

	// Lower-lane image banks, with the same ownership and initialization contract.
	@Memory(Style = MemoryStyle.Block)
	@UninitializedStorage
	lowerMemory: logic[CAPACITY, PIXEL_BITS]

	// Scan-source requests, sampled in the shared clock/reset context.
	public readColumn: in logic[COLUMN_BITS]
	public readRow: in logic[ROW_BITS]
	public frameBoundary: in logic

	// Packed RGB scan responses, black until the first completed presentation.
	public rgbUpper: out logic[PIXEL_BITS]
	public rgbLower: out logic[PIXEL_BITS]

	// Registered RAM responses published without an extra output clock.
	responseUpper: logic[PIXEL_BITS]
	responseLower: logic[PIXEL_BITS]

	// Front-bank selection and presentation request/acknowledgment toggles.
	frontBank: int
	request: bool
	acknowledged: bool
	frontValid: bool
	backInitialized: bool

	// Single-client write payload and completion handshake.
	writeAddress: int
	writeData: logic[PIXEL_BITS]
	writeLower: bool
	writeRequest: bool
	writeAcknowledged: bool

	/**
	 * Write packed R,G,B channels to the back buffer at zero-based (x,y).
	 * Return false outside [0,WIDTH) x [0,HEIGHT), without a write. Return true
	 * only after the memory write is acknowledged. Requires the sole drawing
	 * client; the front image stays unchanged until Present completes.
	 */
	public fn SetPixel(x: int, y: int, rgb: logic[PIXEL_BITS]) bool
	{
		if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) {
			return false
		}

		state {
			// Address = back-bank offset + row within this RGB lane + column.
			writeAddress = (1 - frontBank) * LANE_SIZE + (y & (HEIGHT / 2 - 1)) * WIDTH + x
			writeData = rgb
			writeLower = y >= HEIGHT / 2
			writeRequest = !writeRequest
		}

		while (writeAcknowledged != writeRequest) {
		}

		return true
	}

	/**
	 * Fill every back-buffer pixel and wait for all writes to complete.
	 * Mark this drawing transaction initialized for Present. Call once before
	 * each new image; each R,G,B channel has COLOR_BITS bits. Requires the sole
	 * drawing client.
	 */
	public fn Clear(rgb: logic[PIXEL_BITS])
	{
		for (var y = 0; y < HEIGHT; y++) {
			for (var x = 0; x < WIDTH; x++) {
				SetPixel(x, y, rgb)
			}
		}

		backInitialized = true
	}

	/**
	 * Return false immediately if Clear has not completed for this image.
	 * Otherwise wait for a frame boundary and return true after the banks swap.
	 * The old front bank becomes the back bank and requires another Clear.
	 * The scan source must keep supplying frameBoundary; reset cancels pending
	 * work and invalidates the image. Requires the sole drawing client.
	 */
	public fn Present() bool
	{
		if (!backInitialized) {
			return false
		}

		request = !request

		while (acknowledged != request) {
		}

		backInitialized = false
		return true
	}

	/**
	 * Publish registered RAM responses without adding another accessor clock.
	 */
	process Outputs[]()
	{
		rgbUpper = responseUpper
		rgbLower = responseLower
	}

	/**
	 * Own all RAM writes, front-bank swaps and scan reads in one clocked state.
	 * A pending swap precedes the read on a frame boundary; acknowledged writes
	 * target the back bank. Before the first presentation, scan outputs are black.
	 */
	process MemoryAccess()
	{
		state {
			if (frameBoundary == 0b1 && request != acknowledged) {
				frontBank = 1 - frontBank
				frontValid = true
				acknowledged = request
			}

			if (frontValid) {
				// Both lane reads use front-bank offset + unsigned scan row/column.
				responseUpper = upperMemory[frontBank * LANE_SIZE +
					((readRow as uint) as int) * WIDTH +
					((readColumn as uint) as int)] as logic[PIXEL_BITS]
				responseLower = lowerMemory[frontBank * LANE_SIZE +
					((readRow as uint) as int) * WIDTH +
					((readColumn as uint) as int)] as logic[PIXEL_BITS]
			} else {
				responseUpper = 0 as logic[PIXEL_BITS]
				responseLower = 0 as logic[PIXEL_BITS]
			}

			if (writeRequest != writeAcknowledged) {
				if (writeLower) {
					lowerMemory[writeAddress] = writeData
				} else {
					upperMemory[writeAddress] = writeData
				}

				writeAcknowledged = writeRequest
			}
		}
	}
}