summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/devices/dma.py
blob: 45d03e5c1aa5ad9da2ea6dacd60d9d866e64f638 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""DMA / address-aperture translation seam.

Evidence kind: **hardware address-map header + firmware use + observed
access pattern (through the falcon DMA registers).**

The GSP RISC-V address map (Turing) defines apertures the firmware and its
DMA engine address in GPU physical space:

* ``IMEM``  ``0x0_0000_0000`` (4 GiB window)   -- local instruction memory
* ``DMEM``  ``0x1_0000_0000`` (4 GiB window)   -- local data memory
* ``EMEM``  ``0x2_0000_0000`` (4 GiB window)   -- external memory window
* ``FBGPA`` ``0x6180_0000_0000_0000``          -- framebuffer aperture
* ``SYSGPA````0x8180_0000_0000_0000``          -- system-memory aperture
* ``PRIV``  ``0x2000_0000_0000_0000``          -- register aperture

The simulator keeps local memory in one flat RAM region; framebuffer and
system memory only exist as *profile-declared windows* that map a GPU
physical aperture onto a simulator RAM address.  A window not declared in the
profile is not modelled: the transfer is refused and recorded (never
silently zero-filled).  This class is the single place where that mapping
lives, so a future IOMMU model can replace it without touching devices.
"""
from __future__ import annotations


class DmaTranslator:
    """Translate GPU physical addresses to simulator RAM through windows."""

    def __init__(self, *, ram, ram_base: int, ram_size: int, windows=None):
        self.ram = ram
        self.ram_base = ram_base
        self.ram_size = ram_size
        self.windows: list[tuple[int, int, int]] = []  # (pa_base, sim_base, size)
        self.transfers = 0
        self.refused = 0
        self.events: list[dict] = []
        for win in windows or ():
            self.add_window(int(win["pa"]), int(win["sim"]), int(win["size"]))

    def add_window(self, pa_base: int, sim_base: int, size: int) -> None:
        if size <= 0:
            raise ValueError("DMA window size must be positive")
        self.windows.append((pa_base, sim_base, size))

    def translate(self, pa: int) -> int | None:
        """GPU physical address -> simulator address, or None if unmodelled."""
        for pa_base, sim_base, size in reversed(self.windows):
            if pa_base <= pa < pa_base + size:
                return sim_base + (pa - pa_base)
        return None

    def read(self, pa: int, size: int) -> bytes | None:
        sim = self.translate(pa)
        if sim is None:
            self._refuse(pa, size, "r")
            return None
        return self._read_sim(sim, size)

    def write(self, pa: int, data: bytes) -> bool:
        sim = self.translate(pa)
        if sim is None:
            self._refuse(pa, len(data), "w")
            return False
        self._write_sim(sim, data)
        return True

    def _in_ram(self, addr: int, size: int) -> bool:
        off = addr - self.ram_base
        return 0 <= off and off + size <= self.ram_size

    def _read_sim(self, sim: int, size: int) -> bytes | None:
        if not self._in_ram(sim, size):
            self._refuse(sim, size, "r-sim")
            return None
        off = sim - self.ram_base
        return bytes(self.ram[off:off + size])

    def _write_sim(self, sim: int, data: bytes) -> bool:
        if not self._in_ram(sim, len(data)):
            self._refuse(sim, len(data), "w-sim")
            return False
        off = sim - self.ram_base
        self.ram[off:off + len(data)] = data
        return True

    def copy(self, src_pa: int, dst_pa: int, size: int) -> bool:
        """Copy ``size`` bytes between GPU physical addresses.

        A transfer is counted only when both the source read and the
        destination write complete; a refused destination write leaves the
        transfer in :attr:`refused` and not in :attr:`transfers`.
        """
        data = self.read(src_pa, size)
        if data is None:
            return False
        ok = self.write(dst_pa, data)
        if ok:
            self.transfers += 1
        return ok

    def _refuse(self, pa: int, size: int, kind: str) -> None:
        self.refused += 1
        if len(self.events) < 4096:
            self.events.append({"pa": pa, "size": size, "kind": kind})

    def state(self) -> dict:
        return {
            "windows": [{"pa": p, "sim": s, "size": n} for p, s, n in self.windows],
            "transfers": self.transfers,
            "refused": self.refused,
            "events": list(self.events),
        }

    def reset(self) -> None:
        self.transfers = 0
        self.refused = 0
        self.events.clear()