m1n1/proxyclient/hv/trace_avd.py
Eileen Yoon 16a6b41c98 m1n1/avd: Add initial AVD driver prototype/tracer
Not much to see here, most of the juice is over at:

    https://github.com/eiln/avd.git

The kernel driver (m1n1.fw.avd) only really pipes the instruction stream
into the respective hardware FIFOs and then hushes the interrupt lines.
Most of the work (bitstream syntax parsing and instruction generation)
is done in the avid repo above.

I'm hoping to keep this userland-kernel separation in the very imminent
actual driver.

experiments/avd.py: Decode on the command line. Read file for usage.
experiments/avd_e.py: Decode via emulated instruction stream.
experiments/avd_f.py: Decode via Cortex-M3 firmware (for debugging).
hv/trace_avd.py: Tracer. Read file for usage.
m1n1/fw/avd/__init__.py: Driver base class (power, tunables, etc).
m1n1/fw/avd/decoder.py: Codec-specific decode logic + mini media player.

Signed-off-by: Eileen Yoon <eyn@gmx.com>
2024-01-11 16:20:49 +09:00

224 lines
9.2 KiB
Python

# SPDX-License-Identifier: MIT
from m1n1.trace import Tracer
from m1n1.trace.dart import DARTTracer
from m1n1.utils import *
from m1n1.proxyutils import RegMonitor
hv.p.hv_set_time_stealing(0, 1)
# Usage
#
# 2023/12/25: Only tested on J293AP (AVD revision "Viola"/V3).
# Should work on all baseline M1s.
#
# 1. Start tracer under the hypervisor
#
# 2. Send over the bitstream(s) to the target machine
# Supported formats: .264, .265, .ivf (unless you want to add a demuxer to avid/codecs)
#
# 3. The tracer is purposely not activated at boot. As of 13.5, it takes ~2 HEVC runs
# to get to the login screen - it's probably decoding the login screen. By "activate",
# I mean the tracer variable "outdir" is NULLed s.t. the tracer will not save the
# traced data and merely log the IPC transactions.
#
# [cpu0] [AVDTracer@/arm-io/avd] sent fw command at 0x108eb30
# 00000000 00000801 00030007 0050c000 000002a8 00000003 01091a70 01091a78 00000001
# 00000020 0050caa4 01091af0 01091bc0 01091c50 0050c2a4 0050c210 0050c28c 00000000
# 00000040 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
# [cpu0] [0xfffffe00149d9984] MMIO: W.4 0x269098054 (avd[0], offset 0x1098054) = 0x108eb30
# [cpu7] [0xfffffe00149d99b0] MMIO: R.4 0x269098048 (avd[0], offset 0x1098048) = 0x9
#
# 4. To save the trace contents, break into the hypervisor console and set
#
# >>> tracer.outdir = "matrix_1080X512"
#
# The output data dir will become 'data/[inferred codec name]/$outdir'.
# The data dir will be created if it does not exist. The directory structure is
# meant to look like this:
#
# proxyclient/
# data/
# h264/*
# h265/*
# vp9/*
#
# 5. After the directory name is configured, trigger avd from the target machine:
#
# ffmpeg -hwaccel videotoolbox -i matrix_1080X512.ivf
#
# Or you can access VT directly yourself with some Obj-C (but why would you want to do that..)
# Though I may need to do that to test some sekrit undocumented features.
# The input bitstream is hopefully the matching one as the directory name just set.
#
# 6. If all goes well (i.e. the bitstream is decoding on AVD), the tracer will save:
#
# >> ~/m1n1/proxyclient $ ls data/*
# data/vp9/matrix_1080X512:
# frame.2023-12-17T21:17:47.519048.00004000.bin probs.2023-12-17T21:17:47.519048.00004000.00004000.bin
# frame.2023-12-17T21:17:47.578537.000bc000.bin probs.2023-12-17T21:17:47.578537.000bc000.0000c000.bin
# frame.2023-12-17T21:17:47.633768.00174000.bin probs.2023-12-17T21:17:47.633768.00174000.00014000.bin
# frame.2023-12-17T21:17:47.688067.0022c000.bin probs.2023-12-17T21:17:47.688067.0022c000.0001c000.bin
#
# The "frame" is the macOS source frame_params struct, and this directory is the
# one intended to be supplied to all the avid tools (e.g. emulator, differs).
# For VP9 (and presumably AV1) the tracer will additionally save the "probs" blob.
# You can also bypass FairPlay encryption and save the coded bitstream, but that's
# an exercise left for the reader.
#
# 7. Copy the data directory over to `avd/data/*` & have fun :D
#
# python3 avd_emu.py -i frame.2023-12-17T21:17:47.519048.00004000.bin # emulate single fp
# python3 avd_emu.py -d vp9/matrix_1080X512 -a # emulate all using trace dir name
# python3 tools/test.py -m vp9 -d vp9/matrix_1080X512 -e -a # test against emulated output
#
# Optionally save the firmware to run on the emulator via
# >>> tracer.save_firmware("data/fw.bin")
import datetime
import os
import struct
import time
class AVDTracer(Tracer):
DEFAULT_MODE = TraceMode.SYNC
def __init__(self, hv, dev_path, dart_tracer, verbose=False):
super().__init__(hv, verbose=verbose, ident=type(self).__name__ + "@" + dev_path)
self.dev = hv.adt[dev_path]
self.dart_tracer = dart_tracer
self.base = self.dev.get_reg(0)[0] # 0x268000000
self.p = hv.p
self.u = hv.u
self.dart = dart_tracer.dart
mon = RegMonitor(hv.u)
AVD_REGS = [
#(0x1000000, 0x4000, "unk0"),
#(0x1010000, 0x4000, "dart"),
#(0x1002000, 0x1000, "unk2"),
(0x1070000, 0x4000, "piodma"),
(0x1088000, 0x4000, "sram"),
(0x108c000, 0xc000, "cmd"),
#(0x1098000, 0x4000, "mbox"),
#(0x10a3000, 0x1000, "unka"),
(0x1100000, 0xc000, "config"),
(0x110c000, 0x4000, "dma"),
#(0x1400000, 0x4000, "wrap"),
]
#for (offset, size, name) in AVD_REGS: mon.add(self.base + offset, size, name=name)
self.mon = mon
iomon = RegMonitor(hv.u, ascii=True)
iomon1 = RegMonitor(hv.u, ascii=True)
def readmem_iova(addr, size, readfn=None):
try:
return dart_tracer.dart.ioread(0, addr, size)
except Exception as e:
print(e)
return None
iomon.readmem = readmem_iova
def readmem_iova(addr, size, readfn=None):
try:
return dart_tracer.dart.ioread(1, addr, size)
except Exception as e:
print(e)
return None
iomon1.readmem = readmem_iova
self.iomon = iomon
self.iomon1 = iomon1
self.state_active = False
self.outdir = ""
def avd_r32(self, off): return self.p.read32(self.base + off)
def avd_w32(self, off, x): return self.p.write32(self.base + off, x)
def avd_r64(self, off): return self.p.read64(self.base + off)
def avd_w64(self, off, x): return self.p.write64(self.base + off, x)
def start(self):
self.hv.trace_range(irange(self.dev.get_reg(0)[0], self.dev.get_reg(0)[1]), mode=TraceMode.SYNC)
self.hv.trace_range(irange(self.base + 0x1080000, 0x18000), False)
self.hv.add_tracer(irange(self.base + 0x1098054, 4), "avd-mbox-54", TraceMode.SYNC, self.evt_rw_hook, self.w_AVD_MBOX_0054)
self.hv.add_tracer(irange(self.base + 0x1098064, 4), "avd-mbox-64", TraceMode.SYNC, self.r_AVD_MBOX_0064, self.evt_rw_hook)
def poll(self):
self.mon.poll()
self.iomon.poll()
self.iomon1.poll()
def evt_rw_hook(self, x):
self.poll()
def w_AVD_MBOX_0054(self, x):
if ((x.data >= 0x1080000) and (x.data <= 0x10a0000)):
self.log("Sent fw command at 0x%x" % (x.data))
self.poll()
cmd = self.read_regs(self.base + x.data, 0x60)
chexdump32(cmd)
opcode = struct.unpack("<I", cmd[:4])[0] & 0xf
if (opcode == 0):
self.log("Command start")
self.state_active = True
self.access_idx = 0
elif (opcode == 1):
frame_params_iova = self.p.read32(self.base + x.data + 0x8)
if (self.outdir) and (frame_params_iova != 0x0):
t = datetime.datetime.now().isoformat()
frame_params = self.dart.ioread(1, frame_params_iova, 0xb0000)
word = self.p.read32(self.base + x.data)
if (word & 0x000) == 0x000: # h265
name = "h265"
elif (word & 0x400) == 0x400: # h264
name = "h264"
elif (word & 0x800) == 0x800: # vp9
name = "vp9"
else:
name = "unk"
outdir = os.path.join("data", name, self.outdir)
os.makedirs(outdir, exist_ok=True)
open(os.path.join(outdir, f'frame.{t}.{frame_params_iova:08x}.bin'), "wb").write(frame_params)
if (word & 0x800) == 0x800: # save probs for vp9
iova = [0x4000, 0xc000, 0x14000, 0x1c000][self.access_idx % 4]
open(os.path.join(outdir, f'probs.{t}.{frame_params_iova:08x}.{iova:08x}.bin'), "wb").write(self.dart.ioread(0, iova, 0x4000))
self.access_idx += 1
elif (opcode == 2):
self.log("Command end")
self.state_active = False
self.access_idx = 0
def r_AVD_MBOX_0064(self, x):
if ((x.data >= 0x1080000) and (x.data <= 0x10a0000)):
self.log("Received fw command at 0x%x" % (x.data))
cmd = self.read_regs(self.base + x.data, 0x60)
chexdump32(cmd)
self.poll()
def read_regs(self, addr, size):
scratch = self.u.malloc(size)
p.memcpy32(scratch, addr, size)
return self.p.iface.readmem(scratch, size)
def read_iova(self, start, end, stream=0):
data = b''
for i in range((end - start) // 0x4000):
try:
d = self.dart_tracer.dart.ioread(stream, start + (i * 0x4000), 0x4000)
except:
d = b'\0' * 0x4000
data += d
return data
def save_firmware(self, path="fw.bin"):
firmware = self.read_regs(self.base + 0x1080000, 0x10000)
open(path, "wb").write(firmware)
p.pmgr_adt_clocks_enable('/arm-io/dart-avd')
p.pmgr_adt_clocks_enable('/arm-io/avd')
dart_tracer = DARTTracer(hv, "/arm-io/dart-avd", verbose=0)
dart_tracer.start()
dart = dart_tracer.dart
tracer = AVDTracer(hv, '/arm-io/avd', dart_tracer, verbose=3)
tracer.start()