Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions devices/ramfb/ramfb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Package ramfb implements the fw_cfg DMA RAM framebuffer protocol. Guest
// memory and display capture are supplied by the caller; no host files or
// graphics libraries are required. Calls must be serialized with guest access.
package ramfb

import (
"encoding/binary"
"fmt"
"image"
"math/bits"

"j5.nz/cc/display"
)

type Memory interface {
ReadPhysical(address uint64, dst []byte) error
WritePhysical(address uint64, src []byte) error
}

type Device struct {
base uint64
memory Memory
selector uint16
offset uint64
dmaHigh uint64
config [28]byte
generation uint64
}

func New(base uint64, memory Memory) (*Device, error) {
if base&0xfff != 0 || base > ^uint64(0)-4096 || memory == nil {
return nil, fmt.Errorf("invalid RAMFB aperture or memory")
}
d := &Device{base: base, memory: memory}
return d, nil
}

func (d *Device) Contains(a uint64, size int) bool {
return size > 0 && size <= 8 && a >= d.base && a-d.base <= 4096-uint64(size)
}

func (d *Device) file() ([]byte, bool) {
switch d.selector {
case 0:
return []byte("QEMU"), false
case 1:
return []byte{3, 0, 0, 0}, false
case 0x19:
b := make([]byte, 68)
binary.BigEndian.PutUint32(b, 1)
binary.BigEndian.PutUint32(b[4:], 28)
binary.BigEndian.PutUint16(b[8:], 0x20)
copy(b[12:], "etc/ramfb")
return b, false
case 0x20:
return d.config[:], true
default:
return nil, false
}
}

func (d *Device) Read(a uint64, size int) (uint64, error) {
if !d.Contains(a, size) {
return 0, fmt.Errorf("invalid fw_cfg read")
}
off := a - d.base
if off >= 16 && off+uint64(size) <= 24 {
var sig = [8]byte{'Q', 'E', 'M', 'U', ' ', 'C', 'F', 'G'}
var v uint64
for i := 0; i < size; i++ {
v |= uint64(sig[int(off)-16+i]) << (8 * i)
}
return v, nil
}
if off == 0 && size == 1 {
data, _ := d.file()
var v byte
if d.offset < uint64(len(data)) {
v = data[d.offset]
}
d.offset++
return uint64(v), nil
}
return 0, nil
}

func (d *Device) Write(a uint64, size int, v uint64) error {
if !d.Contains(a, size) {
return fmt.Errorf("invalid fw_cfg write")
}
switch {
case a == d.base+8 && size == 2:
d.selector = bits.ReverseBytes16(uint16(v))
d.offset = 0
case a == d.base+16 && size == 8:
return d.dma(bits.ReverseBytes64(v))
case a == d.base+16 && size == 4:
d.dmaHigh = uint64(bits.ReverseBytes32(uint32(v))) << 32
case a == d.base+20 && size == 4:
return d.dma(d.dmaHigh | uint64(bits.ReverseBytes32(uint32(v))))
}
return nil
}

func (d *Device) dma(a uint64) error {
var desc [16]byte
if err := d.memory.ReadPhysical(a, desc[:]); err != nil {
return err
}
control := binary.BigEndian.Uint32(desc[:])
length := uint64(binary.BigEndian.Uint32(desc[4:]))
target := binary.BigEndian.Uint64(desc[8:])
if control&8 != 0 {
d.selector = uint16(control >> 16)
d.offset = 0
}
status := uint32(0)
data, writable := d.file()
if length > 1<<20 || target+length < target || control&0xffc0 != 0 {
status = 1
} else {
switch control & 0x36 {
case 0: // Selection only.
case 4:
d.offset += length
case 2:
b := make([]byte, length)
if d.offset < uint64(len(data)) {
copy(b, data[d.offset:])
}
if err := d.memory.WritePhysical(target, b); err != nil {
status = 1
} else {
d.offset += length
}
case 16:
if !writable || d.offset > uint64(len(data)) || length > uint64(len(data))-d.offset {
status = 1
break
}
b := make([]byte, length)
if err := d.memory.ReadPhysical(target, b); err != nil {
status = 1
break
}
candidate := d.config
copy(candidate[d.offset:], b)
// The RAMFB configuration is published as one complete record.
if d.offset == 0 && length == 28 {
if err := validate(candidate); err != nil {
status = 1
break
}
}
d.config = candidate
d.offset += length
d.generation++
default:
status = 1
}
}
var result [4]byte
binary.BigEndian.PutUint32(result[:], status)
return d.memory.WritePhysical(a, result[:])
}

// Dimensions returns the guest's committed mode, or zero before configuration.
func (d *Device) Dimensions() (int, int) {
if validate(d.config) != nil {
return 0, 0
}
return int(binary.BigEndian.Uint32(d.config[16:])), int(binary.BigEndian.Uint32(d.config[20:]))
}

func validate(c [28]byte) error {
address := binary.BigEndian.Uint64(c[:])
format := binary.BigEndian.Uint32(c[8:])
flags := binary.BigEndian.Uint32(c[12:])
w, h, stride := uint64(binary.BigEndian.Uint32(c[16:])), uint64(binary.BigEndian.Uint32(c[20:])), uint64(binary.BigEndian.Uint32(c[24:]))
if address == 0 || format != 0x34325258 || flags != 0 || w == 0 || h == 0 || w > 8192 || h > 8192 || stride < w*4 || stride > 1<<20 || stride*h > 256<<20 || address+stride*h < address {
return fmt.Errorf("invalid XRGB8888 RAMFB configuration")
}
return nil
}

// Snapshot copies the configured guest framebuffer while the caller has paused
// guest execution. Pixels follow display.FramebufferUpdate's BGRX contract.
func (d *Device) Snapshot() (display.FramebufferUpdate, error) {
return d.SnapshotInto(nil)
}

// SnapshotInto reuses caller-owned storage for repeated capture. The returned
// pixels alias storage when its capacity is sufficient; callers retain ownership.
// The guest must be stopped throughout the copy.
func (d *Device) SnapshotInto(storage []byte) (display.FramebufferUpdate, error) {
if err := validate(d.config); err != nil {
return display.FramebufferUpdate{}, err
}
a := binary.BigEndian.Uint64(d.config[:])
w := int(binary.BigEndian.Uint32(d.config[16:]))
h := int(binary.BigEndian.Uint32(d.config[20:]))
stride := uint64(binary.BigEndian.Uint32(d.config[24:]))
if cap(storage) < w*h*4 {
storage = make([]byte, w*h*4)
}
pixels := storage[:w*h*4]
if stride == uint64(w*4) {
if err := d.memory.ReadPhysical(a, pixels); err != nil {
return display.FramebufferUpdate{}, err
}
} else {
for y := 0; y < h; y++ {
if err := d.memory.ReadPhysical(a+uint64(y)*stride, pixels[y*w*4:(y+1)*w*4]); err != nil {
return display.FramebufferUpdate{}, err
}
}
}
d.generation++
return display.FramebufferUpdate{Width: w, Height: h, Generation: d.generation, Rect: image.Rect(0, 0, w, h), Pixels: pixels}, nil
}
76 changes: 76 additions & 0 deletions devices/ramfb/ramfb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ramfb

import (
"encoding/binary"
"fmt"
"math/bits"
"testing"
)

type testMemory []byte

func (m testMemory) ReadPhysical(a uint64, b []byte) error {
if a > uint64(len(m)) || uint64(len(b)) > uint64(len(m))-a {
return fmt.Errorf("outside memory")
}
copy(b, m[a:])
return nil
}
func (m testMemory) WritePhysical(a uint64, b []byte) error {
if a > uint64(len(m)) || uint64(len(b)) > uint64(len(m))-a {
return fmt.Errorf("outside memory")
}
copy(m[a:], b)
return nil
}

func TestDriverDMAAndPaddedFramebuffer(t *testing.T) {
mem := make(testMemory, 4096)
d, err := New(0x09020000, mem)
if err != nil {
t.Fatal(err)
}
sig, err := d.Read(0x09020010, 8)
if err != nil || bits.ReverseBytes64(sig) != 0x51454d5520434647 {
t.Fatalf("signature %#x %v", sig, err)
}
dma := func(control uint32, size uint32, address uint64) uint32 {
t.Helper()
binary.BigEndian.PutUint32(mem[16:], control)
binary.BigEndian.PutUint32(mem[20:], size)
binary.BigEndian.PutUint64(mem[24:], address)
if err := d.Write(0x09020010, 8, bits.ReverseBytes64(16)); err != nil {
t.Fatal(err)
}
return binary.BigEndian.Uint32(mem[16:])
}
if status := dma(0x19<<16|10, 4, 128); status != 0 || binary.BigEndian.Uint32(mem[128:]) != 1 {
t.Fatal("directory count")
}
if status := dma(2, 64, 128); status != 0 || string(mem[136:145]) != "etc/ramfb" || binary.BigEndian.Uint16(mem[132:]) != 0x20 {
t.Fatal("directory entry")
}
binary.BigEndian.PutUint64(mem[256:], 1024)
binary.BigEndian.PutUint32(mem[264:], 0x34325258)
binary.BigEndian.PutUint32(mem[272:], 2)
binary.BigEndian.PutUint32(mem[276:], 2)
binary.BigEndian.PutUint32(mem[280:], 12)
if status := dma(0x20<<16|24, 28, 256); status != 0 {
t.Fatal("configuration")
}
copy(mem[1024:], []byte{1, 2, 3, 0, 4, 5, 6, 0})
copy(mem[1036:], []byte{7, 8, 9, 0, 10, 11, 12, 0})
frame, err := d.Snapshot()
if err != nil {
t.Fatal(err)
}
if frame.Width != 2 || frame.Height != 2 || len(frame.Pixels) != 16 || frame.Pixels[8] != 7 {
t.Fatalf("incorrect padded frame: %+v", frame)
}
if status := dma(0x19<<16|24, 4, 128); status != 1 {
t.Fatal("accepted write to read-only directory")
}
if status := dma(0x20<<16|10, 28, 4090); status != 1 {
t.Fatal("accepted invalid DMA destination")
}
}
26 changes: 26 additions & 0 deletions display/gowin/cursor_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build darwin

package gowin

import "github.com/ebitengine/purego/objc"

// Visibility alone preserves absolute mouse motion. Gowin's cursor capture
// also disconnects mouse motion from the host cursor, which is for relative
// input and must not be used for this absolute-pointer presentation.
func cursorVisibility() func(bool) {
hidden := false
cursor := objc.ID(objc.GetClass("NSCursor"))
app := objc.ID(objc.GetClass("NSApplication")).Send(objc.RegisterName("sharedApplication"))
return func(wantHidden bool) {
wantHidden = wantHidden && objc.Send[bool](app, objc.RegisterName("isActive"))
if wantHidden == hidden {
return
}
if wantHidden {
cursor.Send(objc.RegisterName("hide"))
} else {
cursor.Send(objc.RegisterName("unhide"))
}
hidden = wantHidden
}
}
5 changes: 5 additions & 0 deletions display/gowin/cursor_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !darwin

package gowin

func cursorVisibility() func(bool) { return func(bool) {} }
36 changes: 36 additions & 0 deletions display/gowin/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Key mapping shared with the CrumbleCracker desktop frontend.
package gowin

import "github.com/tinyrange/gowin/window"

var keycodes = map[window.Key]uint16{
window.KeyEscape: 1,
window.Key1: 2, window.Key2: 3, window.Key3: 4, window.Key4: 5, window.Key5: 6,
window.Key6: 7, window.Key7: 8, window.Key8: 9, window.Key9: 10, window.Key0: 11,
window.KeyMinus: 12, window.KeyEqual: 13, window.KeyBackspace: 14, window.KeyTab: 15,
window.KeyQ: 16, window.KeyW: 17, window.KeyE: 18, window.KeyR: 19, window.KeyT: 20,
window.KeyY: 21, window.KeyU: 22, window.KeyI: 23, window.KeyO: 24, window.KeyP: 25,
window.KeyLeftBracket: 26, window.KeyRightBracket: 27, window.KeyEnter: 28,
window.KeyLeftControl: 29, window.KeyA: 30, window.KeyS: 31, window.KeyD: 32,
window.KeyF: 33, window.KeyG: 34, window.KeyH: 35, window.KeyJ: 36, window.KeyK: 37,
window.KeyL: 38, window.KeySemicolon: 39, window.KeyApostrophe: 40,
window.KeyGraveAccent: 41, window.KeyLeftShift: 42, window.KeyBackslash: 43,
window.KeyZ: 44, window.KeyX: 45, window.KeyC: 46, window.KeyV: 47, window.KeyB: 48,
window.KeyN: 49, window.KeyM: 50, window.KeyComma: 51, window.KeyPeriod: 52,
window.KeySlash: 53, window.KeyRightShift: 54, window.KeyNumpadMultiply: 55,
window.KeyLeftAlt: 56, window.KeySpace: 57, window.KeyCapsLock: 58,
window.KeyF1: 59, window.KeyF2: 60, window.KeyF3: 61, window.KeyF4: 62,
window.KeyF5: 63, window.KeyF6: 64, window.KeyF7: 65, window.KeyF8: 66,
window.KeyF9: 67, window.KeyF10: 68, window.KeyNumLock: 69, window.KeyScrollLock: 70,
window.KeyNumpad7: 71, window.KeyNumpad8: 72, window.KeyNumpad9: 73,
window.KeyNumpadSubtract: 74, window.KeyNumpad4: 75, window.KeyNumpad5: 76,
window.KeyNumpad6: 77, window.KeyNumpadAdd: 78, window.KeyNumpad1: 79,
window.KeyNumpad2: 80, window.KeyNumpad3: 81, window.KeyNumpad0: 82,
window.KeyNumpadDecimal: 83, window.KeyF11: 87, window.KeyF12: 88,
window.KeyNumpadEnter: 96, window.KeyRightControl: 97, window.KeyNumpadDivide: 98,
window.KeyPrintScreen: 99, window.KeyRightAlt: 100, window.KeyHome: 102,
window.KeyUp: 103, window.KeyPageUp: 104, window.KeyLeft: 105, window.KeyRight: 106,
window.KeyEnd: 107, window.KeyDown: 108, window.KeyPageDown: 109, window.KeyInsert: 110,
window.KeyDelete: 111, window.KeyPause: 119, window.KeyLeftSuper: 125,
window.KeyRightSuper: 126, window.KeyNumpadEqual: 117,
}
Loading
Loading