Compare commits

..

2 commits

Author SHA1 Message Date
saji 2893262f87 more work on fetcher
All checks were successful
Unit Tests / Test (push) Successful in 2m5s
2024-10-20 14:09:35 -05:00
saji 261dedc4dc rework geometry so displaystring position is always topleft screen coord 2024-10-20 12:49:53 -05:00
3 changed files with 93 additions and 23 deletions

View file

@ -22,6 +22,31 @@ logger = logging.getLogger(__name__)
CoordLayout = data.StructLayout({"x": unsigned(32), "y": unsigned(32)}) CoordLayout = data.StructLayout({"x": unsigned(32), "y": unsigned(32)})
class AddressConverter(wiring.Component):
"""Translates display (x,y) into full screen (x,y) based on geometry"""
def __init__(self, geom: DisplayString, *, src_loc_at=0):
self.geom = geom
super().__init__(
{
"input_x": In(geom.dimensions.length),
"addr": In(unsigned(geom.dimensions.addr_bits)),
"output": Out(CoordLayout).array(geom.dimensions.mux),
},
src_loc_at=src_loc_at,
)
def elaborate(self, platform: Platform) -> Module:
m = Module()
for mux in range(self.geom.dimensions.mux):
m.d.comb += self.output[mux].eq(
self.geom.translate_coord(self.input_x, self.addr, mux)
)
return m
class AddressGenerator(wiring.Component): class AddressGenerator(wiring.Component):
"""Generates (x,y) sequences corresponding to a display row.""" """Generates (x,y) sequences corresponding to a display row."""
@ -29,7 +54,8 @@ class AddressGenerator(wiring.Component):
self.geom = geom self.geom = geom
super().__init__( super().__init__(
{ {
"coordstream": Out(stream.Signature(CoordLayout)), "coordstream": Out( stream.Signature(CoordLayout.array(geom.dimensions.mux))
),
"start": In(1), "start": In(1),
"done": Out(1), "done": Out(1),
"addr": In(geom.dimensions.addr_bits), "addr": In(geom.dimensions.addr_bits),
@ -43,21 +69,20 @@ class AddressGenerator(wiring.Component):
counter = Signal(self.geom.dimensions.length) counter = Signal(self.geom.dimensions.length)
# based on the geometry we generate x,y pairs. # based on the geometry we generate x,y pairs.
m.submodules.translate = translate = AddressConverter(self.geom)
with m.FSM(): with m.FSM():
with m.State("init"): with m.State("init"):
m.d.comb += self.done.eq(0) m.d.comb += self.done.eq(0)
m.d.sync += counter.eq(0) m.d.sync += counter.eq(0)
m.d.comb += self.coordstream.valid.eq(0)
with m.If(self.start): with m.If(self.start):
m.next = "run" m.next = "run"
with m.State("run"): with m.State("run"):
if self.geom.rotation == DisplayRotation.LEFTRIGHT: # stream data out as long as it's valid.
# default case, +x.
pass pass
elif self.geom.rotation == DisplayRotation.UPDOWN:
# r 90, +y
pass
with m.State("done"): with m.State("done"):
m.d.comb += self.done.eq(1) m.d.comb += self.done.eq(1)
m.next = "init" m.next = "init"

View file

@ -110,17 +110,17 @@ class DisplayRotation(Enum):
Generally, prefer LEFTRIGHT/UPDOWN over other rotations. Generally, prefer LEFTRIGHT/UPDOWN over other rotations.
""" """
LEFTRIGHT = 0 R0 = 0
UPDOWN = 1 R90 = 1
DOWNUP = 2 R180 = 2
RIGHTLEFT = 3 # why are you like this. R270 = 3 # why are you like this.
@dataclass(frozen=True) @dataclass(frozen=True)
class DisplayString: class DisplayString:
"""Internal class to represent a string of HUB75 displays. """Internal class to represent a string of HUB75 displays.
position: (X,Y) coordinates of the local top-left of the display position: (X,Y) screen coordinates of the top-left of the display.
dimensions: (length, height) local-coordinate dimensions of the display. dimensions: (length, height) local-coordinate dimensions of the display.
@ -129,8 +129,7 @@ class DisplayString:
position: Coord position: Coord
dimensions: DisplayDimensions dimensions: DisplayDimensions
rotation: DisplayRotation rotation: DisplayRotation = DisplayRotation.R0
# TODO: encode muxing
@property @property
def bbox(self) -> BBox: def bbox(self) -> BBox:
@ -140,14 +139,10 @@ class DisplayString:
l = self.dimensions.length l = self.dimensions.length
h = self.dimensions.height h = self.dimensions.height
match self.rotation: match self.rotation:
case DisplayRotation.LEFTRIGHT: case DisplayRotation.R0 | DisplayRotation.R270:
return BBox(Coord(x, y), Coord(x + l, y + h)) return BBox(Coord(x, y), Coord(x + l, y + h))
case DisplayRotation.UPDOWN: case DisplayRotation.R90 | DisplayRotation.R180:
return BBox(Coord(x - h, y), Coord(x, y + l)) return BBox(Coord(x, y), Coord(x + h, y + l))
case DisplayRotation.DOWNUP:
return BBox(Coord(x, y + l), Coord(x + h, y))
case DisplayRotation.RIGHTLEFT:
return BBox(Coord(x - l, y - h), Coord(x, y))
def contains_pix(self, coord: Coord) -> bool: def contains_pix(self, coord: Coord) -> bool:
"""Checks if the given coordinate is inside this display.""" """Checks if the given coordinate is inside this display."""
@ -157,6 +152,38 @@ class DisplayString:
"""Checks if the given BBox intersects with this display""" """Checks if the given BBox intersects with this display"""
return self.bbox.intersects(box) return self.bbox.intersects(box)
def translate_coord(self, pixnum, addr, mux):
"""Helper function to translate string coordinates to screen coordinates"""
assert mux < self.dimensions.mux, "Mux must be within the mux of the display"
x = self.position.x
y = self.position.y
addrshift = addr + (self.dimensions.height // self.dimensions.mux) * mux
#
match self.rotation:
case DisplayRotation.R0:
# x is linear
return {
"x": x + pixnum,
"y": y + addrshift,
}
case DisplayRotation.R90:
# x is height - addrshift
return {
"x": x + self.dimensions.height - addrshift - 1,
"y": y + pixnum,
}
case DisplayRotation.R180:
pass
case DisplayRotation.R270:
# x and y are both length - pixnum
return {
"x": x + self.dimensions.length - 1 - pixnum,
"y": y + addrshift,
}
class DisplayGeometry: class DisplayGeometry:
"""Represents a display based on several strings in different positions.""" """Represents a display based on several strings in different positions."""
@ -185,7 +212,7 @@ class DisplayGeometry:
return sum return sum
def add_string(self, s: DisplayString): def add_string(self, s: DisplayString):
"""Add a new string to the display. """Add a new string to the display.
When in strict mode, this method will throw an exception if this new string When in strict mode, this method will throw an exception if this new string
will overlap with an existing string. will overlap with an existing string.
""" """

View file

@ -1,4 +1,4 @@
from ..geom import Coord, BBox from ..geom import Coord, BBox, DisplayString, DisplayDimensions, DisplayRotation
import pytest import pytest
@ -37,3 +37,21 @@ def test_bbox():
BBox(Coord(0, 0), Coord(1, 0)) BBox(Coord(0, 0), Coord(1, 0))
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
BBox(Coord(1, 1), Coord(0, 0)) BBox(Coord(1, 1), Coord(0, 0))
ds_testdata = [
(DisplayRotation.R0, (0, 0, 0), {"x": 3, "y": 0}),
(DisplayRotation.R0, (40, 2, 0), {"x": 43, "y": 2}),
(DisplayRotation.R0, (40, 2, 1), {"x": 43, "y": 34}),
(DisplayRotation.R90, (40, 0, 0), {"x": 66, "y": 40}),
(DisplayRotation.R90, (120, 2, 0), {"x": 64, "y": 120}),
]
@pytest.mark.parametrize("rot, inp, expected", ds_testdata)
def test_displaystring(rot, inp, expected):
ds = DisplayString(Coord(3, 0), DisplayDimensions(128, 64), rot)
res = ds.translate_coord(*inp)
assert res == expected