diff --git a/devices/ramfb/ramfb.go b/devices/ramfb/ramfb.go new file mode 100644 index 0000000..98d2b50 --- /dev/null +++ b/devices/ramfb/ramfb.go @@ -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 +} diff --git a/devices/ramfb/ramfb_test.go b/devices/ramfb/ramfb_test.go new file mode 100644 index 0000000..2165100 --- /dev/null +++ b/devices/ramfb/ramfb_test.go @@ -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") + } +} diff --git a/display/gowin/cursor_darwin.go b/display/gowin/cursor_darwin.go new file mode 100644 index 0000000..47e9a56 --- /dev/null +++ b/display/gowin/cursor_darwin.go @@ -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 + } +} diff --git a/display/gowin/cursor_other.go b/display/gowin/cursor_other.go new file mode 100644 index 0000000..ad6e5c0 --- /dev/null +++ b/display/gowin/cursor_other.go @@ -0,0 +1,5 @@ +//go:build !darwin + +package gowin + +func cursorVisibility() func(bool) { return func(bool) {} } diff --git a/display/gowin/keys.go b/display/gowin/keys.go new file mode 100644 index 0000000..5a7f76a --- /dev/null +++ b/display/gowin/keys.go @@ -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, +} diff --git a/display/gowin/window.go b/display/gowin/window.go new file mode 100644 index 0000000..b8b1be7 --- /dev/null +++ b/display/gowin/window.go @@ -0,0 +1,257 @@ +// Package gowin presents a display.Session in a native, cgo-free window. +package gowin + +import ( + "context" + "fmt" + "github.com/tinyrange/gowin/gl" + "github.com/tinyrange/gowin/window" + "image" + "j5.nz/cc/display" + "time" + "unsafe" +) + +type Options struct { + Title string + Width, Height int + // Advance optionally advances a cooperatively scheduled VM. It must return + // when its context expires. All session accesses occur on the window thread, + // after Advance returns, avoiding concurrent reads of mutable guest RAM. + Advance func(context.Context) error +} + +// Run owns the window until it is closed. Call from the application's main OS +// thread (lock it in main's init). Closing the window does not close the session. +func Run(ctx context.Context, s display.Session, o Options) error { + if o.Width <= 0 { + o.Width = 1280 + } + if o.Height <= 0 { + o.Height = 832 + } + if o.Title == "" { + o.Title = "Virtual machine" + } + w, err := window.New(o.Title, o.Width, o.Height, true) + if err != nil { + return err + } + defer w.Close() + setCursorHidden := cursorVisibility() + defer setCursorHidden(false) + g, err := w.GL() + if err != nil { + return err + } + program := g.CreateProgram() + defer g.DeleteProgram(program) + for _, src := range []struct { + typ uint32 + text string + }{ + {gl.VertexShader, "#version 150\nin vec2 position; out vec2 uv; void main(){gl_Position=vec4(position,0,1);uv=vec2((position.x+1)/2,(1-position.y)/2);}"}, + {gl.FragmentShader, "#version 150\nuniform sampler2D screen; in vec2 uv; out vec4 color; void main(){color=vec4(texture(screen,uv).rgb,1);}"}, + } { + sh := g.CreateShader(src.typ) + g.ShaderSource(sh, src.text) + g.CompileShader(sh) + var ok int32 + g.GetShaderiv(sh, gl.CompileStatus, &ok) + if ok == 0 { + msg := g.GetShaderInfoLog(sh) + g.DeleteShader(sh) + return fmt.Errorf("display shader: %s", msg) + } + g.AttachShader(program, sh) + g.DeleteShader(sh) + } + g.LinkProgram(program) + var ok int32 + g.GetProgramiv(program, gl.LinkStatus, &ok) + if ok == 0 { + return fmt.Errorf("display program: %s", g.GetProgramInfoLog(program)) + } + g.UseProgram(program) + g.Uniform1i(g.GetUniformLocation(program, "screen"), 0) + var vao, vbo, texture uint32 + g.GenVertexArrays(1, &vao) + defer g.DeleteVertexArrays(1, &vao) + g.BindVertexArray(vao) + g.GenBuffers(1, &vbo) + defer g.DeleteBuffers(1, &vbo) + g.BindBuffer(gl.ArrayBuffer, vbo) + vertices := [8]float32{-1, -1, 1, -1, -1, 1, 1, 1} + g.BufferData(gl.ArrayBuffer, 32, unsafe.Pointer(&vertices[0]), gl.StaticDraw) + pos := uint32(g.GetAttribLocation(program, "position")) + g.EnableVertexAttribArray(pos) + g.VertexAttribPointer(pos, 2, gl.Float, false, 8, 0) + g.GenTextures(1, &texture) + defer g.DeleteTextures(1, &texture) + g.ActiveTexture(gl.Texture0) + g.BindTexture(gl.Texture2D, texture) + g.TexParameteri(gl.Texture2D, gl.TextureMinFilter, gl.Linear) + g.TexParameteri(gl.Texture2D, gl.TextureMagFilter, gl.Linear) + g.TexParameteri(gl.Texture2D, gl.TextureWrapS, gl.ClampToEdge) + g.TexParameteri(gl.Texture2D, gl.TextureWrapT, gl.ClampToEdge) + keys := map[window.Key]bool{} + var buttons uint8 + var lastX, lastY uint32 + defer func() { + for key, down := range keys { + if down { + _ = s.Key(keycodes[key], false) + } + } + if buttons != 0 { + _ = s.Pointer(lastX, lastY, 0, buttons) + } + }() + tw, th := 0, 0 + var generation uint64 + for w.Poll() { + if err := ctx.Err(); err != nil { + return err + } + width, height := s.Size() + bw, bh := w.BackingSize() + + left, top, vw, vh := fit(bw, bh, width, height) + cursorX, cursorY := w.Cursor() + _, _, cursorInside := pointerPosition(cursorX, cursorY, left, top, vw, vh, width, height) + setCursorHidden(cursorInside) + events := w.DrainInputEvents() + for index, e := range events { + if e.Type == window.InputEventMouseMove && index+1 < len(events) && events[index+1].Type == window.InputEventMouseMove { + continue + } + switch e.Type { + case window.InputEventKeyDown, window.InputEventKeyUp, window.InputEventFlagsChanged: + code, known := keycodes[e.Key] + if !known { + continue + } + down := e.Type == window.InputEventKeyDown + if e.Type == window.InputEventFlagsChanged { + down = w.GetKeyState(e.Key).IsDown() + } + if down != keys[e.Key] { + if err := s.Key(code, down); err != nil { + return err + } + keys[e.Key] = down + } + case window.InputEventMouseDown, window.InputEventMouseUp, window.InputEventMouseMove: + if width <= 0 || height <= 0 || vw <= 0 || vh <= 0 { + continue + } + x, y, inside := pointerPosition(e.MouseX, e.MouseY, left, top, vw, vh, width, height) + if !inside && buttons == 0 { + continue + } + previous := buttons + var mask uint8 + switch e.Button { + case window.ButtonLeft: + mask = 1 + case window.ButtonMiddle: + mask = 2 + case window.ButtonRight: + mask = 4 + } + if e.Type == window.InputEventMouseDown { + buttons |= mask + } + if e.Type == window.InputEventMouseUp { + buttons &^= mask + } + if e.Type == window.InputEventMouseMove && uint32(x) == lastX && uint32(y) == lastY { + continue + } + lastX, lastY = uint32(x), uint32(y) + if err := s.Pointer(lastX, lastY, buttons, previous); err != nil { + return err + } + case window.InputEventScroll: + if sc, ok := s.(display.HighResolutionScroller); ok { + if err := sc.Scroll(int32(e.ScrollX*120), int32(e.ScrollY*120)); err != nil { + return err + } + } + } + } + // Focus loss clears Gowin's state. Release held modifiers/buttons so they + // cannot remain stuck in the guest after switching to another application. + for key, down := range keys { + if down && !w.GetKeyState(key).IsDown() { + if err := s.Key(keycodes[key], false); err != nil { + return err + } + keys[key] = false + } + } + previous := buttons + for _, b := range []struct { + k window.Button + m uint8 + }{{window.ButtonLeft, 1}, {window.ButtonMiddle, 2}, {window.ButtonRight, 4}} { + if !w.GetButtonState(b.k).IsDown() { + buttons &^= b.m + } + } + if previous != buttons { + if err := s.Pointer(lastX, lastY, buttons, previous); err != nil { + return err + } + } + if o.Advance != nil { + step, cancel := context.WithTimeout(ctx, 12*time.Millisecond) + err := o.Advance(step) + cancel() + if err != nil { + return err + } + } + f := s.Snapshot(image.Rectangle{}, generation, true) + if len(f.Pixels) > 0 { + if f.Width != tw || f.Height != th { + tw, th = f.Width, f.Height + g.TexImage2D(gl.Texture2D, 0, gl.RGBA, int32(tw), int32(th), 0, 0x80e1, gl.UnsignedByte, nil) + } + g.TexSubImage2D(gl.Texture2D, 0, int32(f.Rect.Min.X), int32(f.Rect.Min.Y), int32(f.Rect.Dx()), int32(f.Rect.Dy()), 0x80e1, gl.UnsignedByte, unsafe.Pointer(&f.Pixels[0])) + generation = f.Generation + } + g.Viewport(0, 0, int32(bw), int32(bh)) + g.ClearColor(0, 0, 0, 1) + g.Clear(gl.ColorBufferBit) + if tw > 0 && th > 0 { + left, top, vw, vh = fit(bw, bh, tw, th) + g.Viewport(int32(left), int32(bh-top-vh), int32(vw), int32(vh)) + g.DrawArrays(gl.TriangleStrip, 0, 4) + } + w.Swap() + } + return nil +} + +func pointerPosition(x, y float32, left, top, w, h, guestW, guestH int) (int, int, bool) { + if w <= 0 || h <= 0 || guestW <= 0 || guestH <= 0 { + return 0, 0, false + } + x -= float32(left) + y -= float32(top) + inside := x >= 0 && y >= 0 && x < float32(w) && y < float32(h) + return max(0, min(guestW-1, int(x*float32(guestW)/float32(w)))), max(0, min(guestH-1, int(y*float32(guestH)/float32(h)))), inside +} +func fit(w, h, gw, gh int) (x, y, vw, vh int) { + if gw <= 0 || gh <= 0 { + return 0, 0, w, h + } + vw = w + vh = w * gh / gw + if vh > h { + vh = h + vw = h * gw / gh + } + return (w - vw) / 2, (h - vh) / 2, vw, vh +} diff --git a/display/gowin/window_test.go b/display/gowin/window_test.go new file mode 100644 index 0000000..b9d37e3 --- /dev/null +++ b/display/gowin/window_test.go @@ -0,0 +1,25 @@ +package gowin + +import "testing" + +func TestPointerMapsRetinaAndLetterboxAndRejectsOutside(t *testing.T) { + left, top, w, h := fit(2560, 1800, 2560, 1664) + for _, test := range []struct { + x, y float32 + wantX, wantY int + inside bool + }{ + {1280, 900, 1280, 832, true}, {0, 68, 0, 0, true}, {2559, 1731, 2559, 1663, true}, + {-10, -10, 0, 0, false}, {1280, 30, 1280, 0, false}, {2600, 1750, 2559, 1663, false}, + } { + x, y, inside := pointerPosition(test.x, test.y, left, top, w, h, 2560, 1664) + if x != test.wantX || y != test.wantY || inside != test.inside { + t.Fatalf("%+v mapped to %d,%d,%v", test, x, y, inside) + } + } + left, top, w, h = fit(1280, 900, 2560, 1664) + x, y, inside := pointerPosition(640, 450, left, top, w, h, 2560, 1664) + if x != 1280 || y != 832 || !inside { + t.Fatalf("scaled center %d,%d,%v", x, y, inside) + } +} diff --git a/display/runner.go b/display/runner.go new file mode 100644 index 0000000..4daba63 --- /dev/null +++ b/display/runner.go @@ -0,0 +1,121 @@ +package display + +import ( + "context" + "image" + "sync" +) + +// Runner serializes VM execution and display access on a worker. Presentation +// does not pause the VM: a session request interrupts advance only long enough +// to copy a frame or submit input. advance must return when its context ends. +// The underlying session's Snapshot result must own its pixels. +type Runner struct { + session Session + ctx context.Context + cancel context.CancelFunc + requests chan func() + done chan struct{} + mu sync.Mutex + wake context.CancelFunc + err error +} + +func StartRunner(ctx context.Context, session Session, advance func(context.Context) error) *Runner { + ctx, cancel := context.WithCancel(ctx) + r := &Runner{session: session, ctx: ctx, cancel: cancel, requests: make(chan func(), 64), done: make(chan struct{})} + go func() { + defer close(r.done) + for ctx.Err() == nil { + step, stop := context.WithCancel(ctx) + r.mu.Lock() + r.wake = stop + r.mu.Unlock() + for { + select { + case request := <-r.requests: + request() + default: + goto drained + } + } + drained: + err := advance(step) + stop() + if err != nil { + r.mu.Lock() + r.err = err + r.mu.Unlock() + return + } + } + }() + return r +} + +func (r *Runner) call(fn func()) bool { + complete := make(chan struct{}) + select { + case r.requests <- func() { fn(); close(complete) }: + case <-r.done: + return false + } + r.mu.Lock() + if r.wake != nil { + r.wake() + } + r.mu.Unlock() + select { + case <-complete: + return true + case <-r.done: + return false + } +} +func (r *Runner) Err() error { r.mu.Lock(); defer r.mu.Unlock(); return r.err } +func (r *Runner) Close() error { r.cancel(); <-r.done; return r.Err() } +func (r *Runner) Size() (w, h int) { r.call(func() { w, h = r.session.Size() }); return } +func (r *Runner) Snapshot(rect image.Rectangle, since uint64, incremental bool) (frame FramebufferUpdate) { + r.call(func() { frame = r.session.Snapshot(rect, since, incremental) }) + return +} +func (r *Runner) Changed() <-chan struct{} { return nil } +func (r *Runner) Resize(w, h int) (err error) { + if !r.call(func() { err = r.session.Resize(w, h) }) { + return r.closedError() + } + return +} +func (r *Runner) Key(code uint16, down bool) (err error) { + if !r.call(func() { err = r.session.Key(code, down) }) { + return r.closedError() + } + return +} +func (r *Runner) Pointer(x, y uint32, buttons, previous uint8) (err error) { + if !r.call(func() { err = r.session.Pointer(x, y, buttons, previous) }) { + return r.closedError() + } + return +} +func (r *Runner) Scroll(x, y int32) (err error) { + if !r.call(func() { + if s, ok := r.session.(HighResolutionScroller); ok { + err = s.Scroll(x, y) + } + }) { + return r.closedError() + } + return +} +func (r *Runner) SetClipboard(text string) { r.call(func() { r.session.SetClipboard(text) }) } +func (r *Runner) GuestClipboard() (text string, generation uint64) { + r.call(func() { text, generation = r.session.GuestClipboard() }) + return +} +func (r *Runner) closedError() error { + if err := r.Err(); err != nil { + return err + } + return context.Canceled +} diff --git a/display/runner_test.go b/display/runner_test.go new file mode 100644 index 0000000..f273d95 --- /dev/null +++ b/display/runner_test.go @@ -0,0 +1,74 @@ +package display + +import ( + "context" + "image" + "sync/atomic" + "testing" + "time" +) + +type serialSession struct { + Session + executing atomic.Bool + keys atomic.Int32 + overlap atomic.Bool +} + +func (s *serialSession) Key(uint16, bool) error { + if s.executing.Load() { + s.overlap.Store(true) + } + s.keys.Add(1) + return nil +} +func (s *serialSession) Snapshot(image.Rectangle, uint64, bool) FramebufferUpdate { + if s.executing.Load() { + s.overlap.Store(true) + } + return FramebufferUpdate{Width: 640, Height: 480} +} + +func TestRunnerInterruptsExecutionForInputAndResumesDuringPresentation(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + s := &serialSession{} + started := make(chan struct{}, 16) + r := StartRunner(ctx, s, func(ctx context.Context) error { + s.executing.Store(true) + select { + case started <- struct{}{}: + default: + } + <-ctx.Done() + s.executing.Store(false) + return nil + }) + defer r.Close() + select { + case <-started: + case <-ctx.Done(): + t.Fatal("VM did not start") + } + if err := r.Key(30, true); err != nil { + t.Fatal(err) + } + frame := r.Snapshot(image.Rectangle{}, 0, false) + if frame.Width != 640 || s.keys.Load() != 1 || s.overlap.Load() { + t.Fatal("display access overlapped execution or lost input") + } + // No presentation callback or sleep is needed to allow execution to resume. + for !s.executing.Load() { + select { + case <-started: + case <-ctx.Done(): + t.Fatal("VM remained paused during presentation") + } + } + if err := r.Close(); err != nil { + t.Fatal(err) + } + if err := r.Key(30, false); err == nil { + t.Fatal("input accepted after close") + } +} diff --git a/go.mod b/go.mod index 54bf2cb..5ac3ce1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.5 require ( github.com/creack/pty v1.1.24 github.com/ebitengine/purego v0.10.0 + github.com/tinyrange/gowin v0.0.0-20260809233042-3f02e7d42a2d ) require ( diff --git a/go.sum b/go.sum index eb4747d..1b0e495 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinyrange/gowin v0.0.0-20260809233042-3f02e7d42a2d h1:Ub95g/zolrSuVGAkN/IlfUMMzXeJc1UnnOCXqRHbnok= +github.com/tinyrange/gowin v0.0.0-20260809233042-3f02e7d42a2d/go.mod h1:SCECczSGPwH3/pZhm1dNuuUYh/vtu6YLV9Rj/L67MB4= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= diff --git a/hypervisor/README.md b/hypervisor/README.md new file mode 100644 index 0000000..0d82c6d --- /dev/null +++ b/hypervisor/README.md @@ -0,0 +1,58 @@ +# Caller-constructed ARM64 machines + +`hypervisor.NewARM64(ctx)` exposes native execution without imposing a kernel, +firmware, disk-image format or host-path convention. The initial implementation +uses macOS Hypervisor.framework on Apple Silicon with its native GIC. It builds +with `CGO_ENABLED=0`; an entitled executable is required. + +1. Map caller-selected RAM ranges with `MapRAM` (16 KiB alignment on HVF). +2. Fill the returned bytes and `Restore` an `ARM64State` containing general, + SIMD, PC/PSTATE, floating-point control and architectural system registers. +3. Call `Run(ctx)`, inspect `Exit` and dispatch only the devices/services that + the caller has installed. `EmulateMMIO` implements AArch64 load/store abort + register semantics, including sign extension and XZR. +4. Close the VM when finished. RAM slices become invalid on close. + +Register indices 0–30 select X0–X30; use `RegisterPC`, `RegisterPState`, +`RegisterFPCR` and `RegisterFPSR` for the other exposed registers. System +registers use architectural op0:op1:CRn:CRm:op2 encodings. Unsupported host +registers return errors. `Restore` does not claim to restore GIC, device, +virtual-counter offsets or a complete migration snapshot. + +Methods must be serialized, except `Cancel`, which may interrupt `Run` from +another goroutine. Device interrupts can produce `ExitCanceled`; callers should +resume these when their own context is still live. Exception metadata is valid +only for `ExitException`. `Counter` reports HVF's virtual-counter clock and Hz. + +`HandlePSCI` services PSCI 1.1 and SMCCC 1.1 discovery over HVC. Feature queries +report supported calls; unsupported suspend modes and architecture services +return NOT_SUPPORTED. Its boolean result identifies a terminal SYSTEM_OFF or +SYSTEM_RESET request. The caller must stop execution rather than resume past +that request; adapters can expose `ExitShutdown` and `ExitReset` to their users. + +`NewNVMePCI` binds caller-owned random-access storage to an ECAM bus and one +32-bit memory BAR. Interrupt numbers are architectural GIC IDs. The caller must +provide matching firmware declarations and dispatch the returned MMIO device. +`devices/ramfb` is a separate portable fw_cfg DMA device using a caller-provided +physical-memory interface and the public `display.FramebufferUpdate` format. + +`AddInputPCI` adds modern virtio keyboard or absolute-pointer functions to that +bus before guest enumeration. Its `InputDevice` accepts Linux key codes, guest +pixel coordinates, buttons and wheel events. The transport uses guest-owned +split virtqueues and INTx; firmware must describe each function's interrupt. +`Stats` gives a bounded view of queue readiness and event consumption. + +`display/gowin` is a native presenter for any `display.Session`, using +`github.com/tinyrange/gowin` with cgo disabled. Start it on the initial OS thread. +`display.StartRunner` adapts a cooperatively interruptible VM to a display +session whose requests serialize with execution on a worker. The VM continues +running during rendering; snapshots must own their returned pixel data. + +The optional Windows debug handling and UEFI runtime policy live in trex, not +in this package. The low-level backend provides debug trapping and architectural +BRK delivery without knowing a guest operating system. + +The entitled integration test exercises general/SIMD state restoration, the +native cycle counter and debug exception delivery. Build the test executable +with cgo disabled, sign with `tools/entitlements.xml`, and run +`CC_HVF_TEST=1 -test.run TestARM64Restore`. diff --git a/hypervisor/arm64.go b/hypervisor/arm64.go new file mode 100644 index 0000000..8028a68 --- /dev/null +++ b/hypervisor/arm64.go @@ -0,0 +1,79 @@ +// Package hypervisor exposes in-process execution of caller-constructed machines. +// Inputs are architectural state and memory, with no image or host-path policy. +package hypervisor + +import ( + "context" + "io" +) + +type BlockDevice interface { + io.ReaderAt + io.WriterAt + Size() int64 +} +type MMIODevice interface { + Contains(address uint64, size int) bool + Read(address uint64, size int) (uint64, error) + Write(address uint64, size int, value uint64) error +} + +// NVMePCIConfiguration describes an ECAM bus and one NVMe function. Interrupt is +// a GIC interrupt ID, including the 32 interrupt offset for SPIs. +type NVMePCIConfiguration struct { + ConfigBase, ConfigSize, WindowBase, WindowSize, BAR uint64 + Device uint8 + Interrupt uint32 +} + +type ARM64State struct { + X [31]uint64 + Q [32][16]byte + PC, PState, FPCR, FPSR uint64 + // System uses architectural op0:op1:CRn:CRm:op2 register encodings. + System map[uint16]uint64 +} + +type Exit struct { + Reason uint32 + PC, Syndrome, VirtualAddress, PhysicalAddress uint64 +} + +const ( + ExitCanceled uint32 = iota + ExitException + ExitVirtualTimer + ExitUnknown + // Terminal platform requests, synthesized by an execution adapter. + ExitShutdown + ExitReset +) + +const ( + RegisterPC uint32 = 31 + RegisterFPCR uint32 = 32 + RegisterFPSR uint32 = 33 + RegisterPState uint32 = 34 +) + +// ARM64 owns its mapped RAM and VCPU. Methods other than Cancel must be serialized. +type ARM64 interface { + MapRAM(base, size uint64) ([]byte, error) + Restore(ARM64State) error + Run(context.Context) (Exit, error) + Register(index uint32) (uint64, error) + SetRegister(index uint32, value uint64) error + SystemRegister(encoding uint16) (uint64, error) + // Counter reads the native virtual counter and its frequency in Hz. + Counter() (ticks, frequency uint64, err error) + HandleSystemInstruction(syndrome uint64) (bool, error) + HandlePSCI() (bool, error) + SetDebugExceptionTrapping(bool) error + DeliverDebugException(Exit) error + NewNVMePCI(NVMePCIConfiguration, BlockDevice) (MMIODevice, error) + EmulateMMIO(Exit, MMIODevice) (bool, error) + InterruptState() (map[string]uint64, error) + Advance() error + Cancel() error + Close() error +} diff --git a/hypervisor/arm64_darwin.go b/hypervisor/arm64_darwin.go new file mode 100644 index 0000000..4c9f0d6 --- /dev/null +++ b/hypervisor/arm64_darwin.go @@ -0,0 +1,233 @@ +//go:build darwin && arm64 + +package hypervisor + +import ( + "context" + "fmt" + "j5.nz/cc/internal/hv/hvf" + "sort" +) + +type arm64VM struct{ vm *hvf.VM } + +func hostCounterFrequency() uint64 + +func NewARM64(ctx context.Context) (ARM64, error) { + vm, err := hvf.NewVMWithContext(ctx) + if err != nil { + return nil, err + } + return &arm64VM{vm}, nil +} +func (v *arm64VM) MapRAM(base, size uint64) ([]byte, error) { + if size == 0 || size > 1<<40 || base+size < base || base&0x3fff != 0 || size&0x3fff != 0 { + return nil, fmt.Errorf("invalid RAM range") + } + return v.vm.MapAnonymousMemory(uintptr(size), hvf.IPA(base), 7) +} +func (v *arm64VM) Restore(s ARM64State) error { + for i, x := range s.X { + if err := v.vm.SetReg(hvf.Reg(i), x); err != nil { + return err + } + } + for i, q := range s.Q { + if err := v.vm.SetVector(i, q); err != nil { + return err + } + } + keys := make([]int, 0, len(s.System)) + for k := range s.System { + keys = append(keys, int(k)) + } + sort.Ints(keys) + for _, k := range keys { + if err := v.vm.SetSysReg(hvf.SysReg(k), s.System[uint16(k)]); err != nil { + return fmt.Errorf("restore system register %#x: %w", k, err) + } + } + for _, r := range []struct { + id hvf.Reg + value uint64 + }{{31, s.PC}, {34, s.PState}, {32, s.FPCR}, {33, s.FPSR}} { + if err := v.vm.SetReg(r.id, r.value); err != nil { + return err + } + } + return nil +} +func (v *arm64VM) Run(ctx context.Context) (Exit, error) { + if err := ctx.Err(); err != nil { + return Exit{}, err + } + done := make(chan struct{}) + stop := context.AfterFunc(ctx, func() { _ = v.vm.CancelRun(); close(done) }) + ex, err := v.vm.Run() + if !stop() { + <-done + } + if err != nil { + return Exit{}, err + } + pc, err := v.vm.GetProgramCounter() + if err != nil { + return Exit{}, err + } + if ex.Reason != 1 { + return Exit{Reason: uint32(ex.Reason), PC: pc}, nil + } + return Exit{uint32(ex.Reason), pc, ex.Exception.Syndrome, ex.Exception.VirtualAddress, uint64(ex.Exception.PhysicalAddress)}, nil +} +func (v *arm64VM) Register(i uint32) (uint64, error) { return v.vm.GetReg(hvf.Reg(i)) } +func (v *arm64VM) SetRegister(i uint32, x uint64) error { return v.vm.SetReg(hvf.Reg(i), x) } +func (v *arm64VM) SystemRegister(i uint16) (uint64, error) { return v.vm.GetSysReg(hvf.SysReg(i)) } +func (v *arm64VM) HandleSystemInstruction(s uint64) (bool, error) { + return v.vm.HandleSystemInstruction(s) +} +func (v *arm64VM) Advance() error { return v.vm.AdvanceProgramCounter() } +func (v *arm64VM) HandlePSCI() (bool, error) { return v.vm.HandlePSCI() } +func (v *arm64VM) SetDebugExceptionTrapping(enabled bool) error { + return v.vm.SetDebugExceptionTrapping(enabled) +} + +// DeliverDebugException reinjects a trapped AArch64 BRK into EL1. It leaves +// general and SIMD registers intact and selects the architectural vector for +// the source exception level and stack selection. +func (v *arm64VM) DeliverDebugException(ex Exit) error { + if ex.Reason != 1 || ex.Syndrome>>26 != 0x3c { + return fmt.Errorf("expected AArch64 BRK exit") + } + state, err := v.vm.GetReg(34) + if err != nil { + return err + } + mode := state & 0x1f + if mode != 0 && mode != 4 && mode != 5 { + return fmt.Errorf("unsupported exception source mode %#x", mode) + } + vbar, err := v.vm.GetSysReg(0xc600) + if err != nil { + return err + } + sctlr, err := v.vm.GetSysReg(0xc080) + if err != nil { + return err + } + vector := vbar + if mode == 0 { + vector += 0x400 + } else if mode == 5 { + vector += 0x200 + } + for _, r := range []struct { + reg hvf.SysReg + value uint64 + }{{0xc201, ex.PC}, {0xc200, state}, {0xc290, ex.Syndrome}} { + if err := v.vm.SetSysReg(r.reg, r.value); err != nil { + return err + } + } + next := state&^uint64(0x1f|1<<20|1<<21|1<<23|3<<10) | 0x3c5 + if sctlr&(1<<23) == 0 { + next |= 1 << 22 + } + if err := v.vm.SetReg(34, next); err != nil { + return err + } + return v.vm.SetReg(31, vector) +} +func (v *arm64VM) Cancel() error { return v.vm.CancelRun() } +func (v *arm64VM) Close() error { return v.vm.Close() } + +func (v *arm64VM) InterruptState() (map[string]uint64, error) { + out := map[string]uint64{} + frequency := hostCounterFrequency() + ticks := v.vm.CounterTicks() + offset, err := v.vm.GetVTimerOffset() + if err != nil { + return nil, err + } + out["counter"], out["frequency"], out["counter_offset"] = ticks-offset, frequency, offset + masked, err := v.vm.GetVTimerMask() + if err != nil { + return nil, err + } + if masked { + out["timer_masked"] = 1 + } + for name, reg := range map[string]hvf.GICRedistributorReg{"group": 0x10080, "enabled": 0x10100, "pending": 0x10200, "active": 0x10300} { + value, err := v.vm.GetGICRedistributorReg(reg) + if err != nil { + return nil, err + } + out[name] = value + } + for name, reg := range map[string]hvf.GICICCReg{"priority_mask": 0xc230, "group1_enable": 0xc667} { + value, err := v.vm.GetGICICCReg(reg) + if err != nil { + return nil, err + } + out[name] = value + } + return out, nil +} + +func (v *arm64VM) Counter() (uint64, uint64, error) { + offset, err := v.vm.GetVTimerOffset() + if err != nil { + return 0, 0, err + } + return v.vm.CounterTicks() - offset, hostCounterFrequency(), nil +} + +func (v *arm64VM) NewNVMePCI(c NVMePCIConfiguration, d BlockDevice) (MMIODevice, error) { + return v.vm.NewNVMePCI(c.ConfigBase, c.ConfigSize, c.WindowBase, c.WindowSize, c.BAR, c.Device, c.Interrupt, d) +} + +func (v *arm64VM) EmulateMMIO(ex Exit, d MMIODevice) (bool, error) { + if ex.Reason != 1 || ex.Syndrome>>26 != 0x24 { + return false, nil + } + info, err := hvf.DecodeDataAbort(ex.Syndrome) + if err != nil { + return false, err + } + if !d.Contains(ex.PhysicalAddress, info.SizeBytes) { + return false, nil + } + if info.Write { + var value uint64 + if uint32(info.Target) != 0xffffffff { + value, err = v.vm.GetReg(info.Target) + if err != nil { + return false, err + } + } + if err := d.Write(ex.PhysicalAddress, info.SizeBytes, value); err != nil { + return false, err + } + } else { + value, err := d.Read(ex.PhysicalAddress, info.SizeBytes) + if err != nil { + return false, err + } + if ex.Syndrome&(1<<21) != 0 { + shift := 64 - info.SizeBytes*8 + value = uint64(int64(value<> shift) + } + if ex.Syndrome&(1<<15) == 0 { + value = uint64(uint32(value)) + } + if uint32(info.Target) != 0xffffffff { + if err := v.vm.SetReg(info.Target, value); err != nil { + return false, err + } + } + } + return true, v.vm.AdvanceProgramCounter() +} + +func (v *arm64VM) addInputPCI(bus MMIODevice, c InputPCIConfiguration) (InputDevice, error) { + return v.vm.AddInputPCI(bus, c.Device, c.BAR, c.Interrupt, c.Pointer, c.Width, c.Height) +} diff --git a/hypervisor/arm64_darwin_test.go b/hypervisor/arm64_darwin_test.go new file mode 100644 index 0000000..f92fe65 --- /dev/null +++ b/hypervisor/arm64_darwin_test.go @@ -0,0 +1,149 @@ +//go:build darwin && arm64 + +package hypervisor + +import ( + "context" + "encoding/binary" + "os" + "testing" + "time" +) + +func TestARM64Restore(t *testing.T) { + if os.Getenv("CC_HVF_TEST") != "1" { + t.Skip("requires hypervisor-entitled test executable") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + vm, err := NewARM64(ctx) + if err != nil { + t.Fatal(err) + } + defer vm.Close() + ram, err := vm.MapRAM(0x40000000, 0x10000) + if err != nil { + t.Fatal(err) + } + // STR Q0,[X0]; STR Q31,[X0,#16]; ADD X1,X1,#1; + // MRS X2,PMCCNTR_EL0; MRS X3,CNTVCT_EL0; HVC #0. + for i, w := range []uint32{0x3d800000, 0x3d80041f, 0x91000421, 0xd53b9d02, 0xd53be043, 0xd4000002} { + binary.LittleEndian.PutUint32(ram[i*4:], w) + } + state := ARM64State{PC: 0x40000000, PState: 0x3c5, System: map[uint16]uint64{0xc082: 3 << 20}} + dfr, err := vm.SystemRegister(0xc028) + if err != nil { + t.Fatal(err) + } + state.System[0xc028] = dfr&^uint64(0xf00) | 0x100 + state.X[0] = 0x40001000 + state.X[1] = 41 + for i := range state.Q[0] { + state.Q[0][i] = byte(i + 1) + state.Q[31][i] = byte(255 - i) + } + if err := vm.Restore(state); err != nil { + t.Fatal(err) + } + before, frequency, err := vm.Counter() + if err != nil || frequency == 0 { + t.Fatalf("counter frequency: %d %v", frequency, err) + } + ex, err := vm.Run(ctx) + if err != nil { + t.Fatal(err) + } + if ex.Syndrome>>26 != 0x16 { + t.Fatalf("exit: %+v", ex) + } + after, _, err := vm.Counter() + if err != nil { + t.Fatal(err) + } + guestCounter, err := vm.Register(3) + if err != nil || guestCounter < before || guestCounter > after { + t.Fatalf("guest counter %d outside native interval [%d,%d]: %v", guestCounter, before, after, err) + } + x1, err := vm.Register(1) + if err != nil || x1 != 42 { + t.Fatalf("x1=%d: %v", x1, err) + } + for i := 0; i < 16; i++ { + if ram[0x1000+i] != state.Q[0][i] || ram[0x1010+i] != state.Q[31][i] { + t.Fatalf("SIMD byte %d not restored", i) + } + } + // A trapped divide breakpoint must be deliverable to the guest vector + // without losing the interrupted PC or PSTATE. + binary.LittleEndian.PutUint32(ram[:], 0xd43e0080) + binary.LittleEndian.PutUint32(ram[0x2200:], 0xd4000002) + state.System[0xc600] = 0x40002000 + if err := vm.Restore(state); err != nil { + t.Fatal(err) + } + if err := vm.SetDebugExceptionTrapping(true); err != nil { + t.Fatal(err) + } + ex, err = vm.Run(ctx) + if err != nil || ex.Syndrome>>26 != 0x3c { + t.Fatalf("BRK: %+v %v", ex, err) + } + if err := vm.DeliverDebugException(ex); err != nil { + t.Fatal(err) + } + ex, err = vm.Run(ctx) + if err != nil || ex.Syndrome>>26 != 0x16 || ex.PC != 0x40002204 { + t.Fatalf("vector: %+v %v", ex, err) + } + elr, err := vm.SystemRegister(0xc201) + if err != nil || elr != state.PC { + t.Fatalf("ELR=%#x: %v", elr, err) + } + saved, err := vm.SystemRegister(0xc200) + if err != nil || saved != state.PState { + t.Fatalf("SPSR=%#x: %v", saved, err) + } +} + +func TestARM64PSCIPowerDiscovery(t *testing.T) { + if os.Getenv("CC_HVF_TEST") != "1" { + t.Skip("requires hypervisor-entitled test executable") + } + vm, err := NewARM64(context.Background()) + if err != nil { + t.Fatal(err) + } + defer vm.Close() + for _, call := range []struct { + function, argument, want uint64 + terminal bool + }{ + {0x84000000, 0, 0x10001, false}, + {0x80000000, 0, 0x10001, false}, + {0x8400000a, 0x80000000, 0, false}, + {0x8400000a, 0x84000008, 0, false}, + {0x8400000a, 0x84000009, 0, false}, + {0x8400000a, 0xc4000001, 0xffffffff, false}, + {0x8400000a, 0xffffffff, 0xffffffff, false}, + {0x80000001, 0x80000000, 0, false}, + {0x80000001, 0x80000001, 0, false}, + {0x80000001, 0x80008000, 0xffffffff, false}, + {0x84000008, 0, 0x84000008, true}, + {0x84000009, 0, 0x84000009, true}, + } { + if err := vm.SetRegister(0, call.function); err != nil { + t.Fatal(err) + } + if err := vm.SetRegister(1, call.argument); err != nil { + t.Fatal(err) + } + terminal, err := vm.HandlePSCI() + if err != nil || terminal != call.terminal { + t.Fatalf("call %#x/%#x: terminal %v err %v", call.function, call.argument, terminal, err) + } + result, err := vm.Register(0) + if err != nil || result != call.want { + t.Fatalf("call %#x/%#x: result %#x want %#x err %v", call.function, call.argument, result, call.want, err) + } + } +} diff --git a/hypervisor/arm64_other.go b/hypervisor/arm64_other.go new file mode 100644 index 0000000..d37a405 --- /dev/null +++ b/hypervisor/arm64_other.go @@ -0,0 +1,12 @@ +//go:build !darwin || !arm64 + +package hypervisor + +import ( + "context" + "fmt" +) + +func NewARM64(context.Context) (ARM64, error) { + return nil, fmt.Errorf("ARM64 native continuation requires darwin/arm64") +} diff --git a/hypervisor/counter_darwin_arm64.s b/hypervisor/counter_darwin_arm64.s new file mode 100644 index 0000000..1fe407b --- /dev/null +++ b/hypervisor/counter_darwin_arm64.s @@ -0,0 +1,6 @@ +#include "textflag.h" + +TEXT ·hostCounterFrequency(SB), NOSPLIT, $0-8 + MRS CNTFRQ_EL0, R0 + MOVD R0, ret+0(FP) + RET diff --git a/hypervisor/input.go b/hypervisor/input.go new file mode 100644 index 0000000..b00ea61 --- /dev/null +++ b/hypervisor/input.go @@ -0,0 +1,32 @@ +package hypervisor + +import "fmt" + +// InputDevice delivers Linux input-event keycodes and absolute pointer positions +// into guest-owned virtqueues, with hardware interrupts. Calls must be serialized +// with Run and MMIO dispatch by the owner of the VM. +type InputDevice interface { + Key(code uint16, down bool) error + PointerEvent(x, y uint32, buttons, previous uint8) error + ScrollEvent(x120, y120 int32) error + SetDimensions(width, height uint32) + Stats() map[string]uint64 +} +type InputPCIConfiguration struct { + Device uint8 + BAR uint64 + Interrupt uint32 + Pointer bool + Width, Height uint32 +} + +// AddInputPCI adds a modern virtio input function to a bus returned by NewNVMePCI. +// Call before running the VM so guest enumeration observes a stable topology. +func AddInputPCI(vm ARM64, bus MMIODevice, c InputPCIConfiguration) (InputDevice, error) { + if v, ok := vm.(interface { + addInputPCI(MMIODevice, InputPCIConfiguration) (InputDevice, error) + }); ok { + return v.addInputPCI(bus, c) + } + return nil, fmt.Errorf("PCI input unavailable on this backend") +} diff --git a/internal/hv/hvf/bindings_darwin_arm64.go b/internal/hv/hvf/bindings_darwin_arm64.go index 5678508..bc374f0 100644 --- a/internal/hv/hvf/bindings_darwin_arm64.go +++ b/internal/hv/hvf/bindings_darwin_arm64.go @@ -170,12 +170,13 @@ const ( ) var ( - loadOnce sync.Once - loadErr error - vmLifecycleMu sync.Mutex - activeVM atomic.Pointer[VM] - hvLib uintptr - sysLib uintptr + loadOnce sync.Once + loadErr error + vmLifecycleMu sync.Mutex + activeVM atomic.Pointer[VM] + hvLib uintptr + sysLib uintptr + machAbsoluteTime func() uint64 hvVMConfigCreate func() VMConfig hvVMConfigGetEL2Supported func(el2Supported *bool) Return @@ -274,6 +275,7 @@ func load() error { registerOptionalLibFunc(&taskSelfTrap, sysLib, "task_self_trap") purego.RegisterLibFunc(&osRelease, sysLib, "os_release") + purego.RegisterLibFunc(&machAbsoluteTime, sysLib, "mach_absolute_time") }) return loadErr } @@ -1648,6 +1650,36 @@ func (v *VM) HandleSystemInstructionForVCPU(index int, syndrome uint64) (bool, e return handled, err } default: + // Debug-exception trapping also routes the architectural breakpoint + // and watchpoint registers through the host. Preserve guest setup in + // the actual VCPU so these retain their architectural behavior. + reg := info.SysReg() + if reg&0xff00 == 0x8000 && reg&7 >= 4 { + if info.Read { + value, err := v.GetSysRegForVCPU(index, reg) + if err != nil { + return false, err + } + if info.Rt != hvRegXZR { + if err := v.SetRegForVCPU(index, info.Rt, value); err != nil { + return false, err + } + } + } else { + var value uint64 + if info.Rt != hvRegXZR { + var err error + value, err = v.GetRegForVCPU(index, info.Rt) + if err != nil { + return false, err + } + } + if err := v.SetSysRegForVCPU(index, reg, value); err != nil { + return false, err + } + } + return true, v.AdvanceProgramCounterForVCPU(index) + } return false, nil } diff --git a/internal/hv/hvf/container_darwin_arm64.go b/internal/hv/hvf/container_darwin_arm64.go index b8fd875..515e8b1 100644 --- a/internal/hv/hvf/container_darwin_arm64.go +++ b/internal/hv/hvf/container_darwin_arm64.go @@ -2366,6 +2366,8 @@ func handleContainerHVC(vm *VM, vcpuIndex int) (bool, error) { psciSystemOff = 0x84000008 psciSystemReset = 0x84000009 psciFeatures = 0x8400000a + smcccVersion = 0x80000000 + smcccArchFeatures = 0x80000001 psciSuccess = 0 psciNotSupported = 0xffffffff psciInvalidParams = 0xfffffffe @@ -2377,11 +2379,30 @@ func handleContainerHVC(vm *VM, vcpuIndex int) (bool, error) { var ret uint64 switch x0 { case psciVersion: - ret = 0x00010000 + ret = 0x00010001 + case smcccVersion: + ret = 0x00010001 + case smcccArchFeatures: + function, err := vm.GetRegForVCPU(vcpuIndex, hvRegX1) + if err != nil { + return false, err + } + ret = psciNotSupported + if function == smcccVersion || function == smcccArchFeatures { + ret = psciSuccess + } case psciMigrateInfoType: ret = psciTosNotPresent case psciFeatures: ret = psciNotSupported + function, err := vm.GetRegForVCPU(vcpuIndex, hvRegX1) + if err != nil { + return false, err + } + switch function { + case psciVersion, psciCpuOff, psciCpuOn, psciCpuOn64, psciAffinityInfo, psciAffinityInfo64, psciMigrateInfoType, psciSystemOff, psciSystemReset, psciFeatures, smcccVersion: + ret = psciSuccess + } case psciCpuSuspend: ret = psciNotSupported case psciCpuOff: diff --git a/internal/hv/hvf/counter_darwin_arm64.go b/internal/hv/hvf/counter_darwin_arm64.go new file mode 100644 index 0000000..3fd7658 --- /dev/null +++ b/internal/hv/hvf/counter_darwin_arm64.go @@ -0,0 +1,8 @@ +//go:build darwin && arm64 + +package hvf + +// CounterTicks returns the host clock used by HVF for CNTVCT_EL0 before +// subtracting the vCPU's virtual timer offset. The physical ARM counter is +// a different clock domain on macOS. +func (v *VM) CounterTicks() uint64 { return machAbsoluteTime() } diff --git a/internal/hv/hvf/debug_darwin_arm64.go b/internal/hv/hvf/debug_darwin_arm64.go new file mode 100644 index 0000000..1f2ba76 --- /dev/null +++ b/internal/hv/hvf/debug_darwin_arm64.go @@ -0,0 +1,34 @@ +//go:build darwin && arm64 + +package hvf + +import ( + "fmt" + "github.com/ebitengine/purego" + "sync" +) + +var debugBinding struct { + once sync.Once + trap func(VCPU, bool) Return +} + +func bindDebug() { + debugBinding.once.Do(func() { + purego.RegisterLibFunc(&debugBinding.trap, hvLib, "hv_vcpu_set_trap_debug_exceptions") + }) +} + +// SetDebugExceptionTrapping controls whether guest breakpoints and debug +// exceptions exit to the caller instead of being delivered to the guest. +func (v *VM) SetDebugExceptionTrapping(enabled bool) error { + bindDebug() + var result Return + if err := v.callOnThread(func() { result = debugBinding.trap(v.vcpus[0].vcpu, enabled) }); err != nil { + return err + } + if result != hvSuccess { + return fmt.Errorf("trap debug exceptions: %w", result) + } + return nil +} diff --git a/internal/hv/hvf/devices_darwin_arm64.go b/internal/hv/hvf/devices_darwin_arm64.go new file mode 100644 index 0000000..1e0ee7a --- /dev/null +++ b/internal/hv/hvf/devices_darwin_arm64.go @@ -0,0 +1,46 @@ +//go:build darwin && arm64 + +package hvf + +import ( + "fmt" + "j5.nz/cc/internal/nvme" + "j5.nz/cc/internal/virtio" +) + +// NewNVMePCI builds a PCI controller over caller-owned random-access storage. +// The caller owns the returned MMIO device and dispatches its accesses while +// this VM is stopped. Interrupt is a GIC interrupt ID, not an SPI index. +func (v *VM) NewNVMePCI(configBase, configSize, windowBase, windowSize, bar uint64, device uint8, interrupt uint32, disk virtio.BlockBackend) (*hvfPCIHost, error) { + if disk == nil || configBase&0xfffff != 0 || configSize == 0 || configSize > 256<<20 || configSize&0xfffff != 0 || configBase+configSize < configBase || windowSize == 0 || windowBase+windowSize < windowBase || windowBase+windowSize > 1<<32 || configBase < windowBase+windowSize && windowBase < configBase+configSize || device > 31 || interrupt < 32 || interrupt > 255 || bar&0x3fff != 0 || bar < windowBase || bar > windowBase+windowSize || nvme.MMIOSize > windowBase+windowSize-bar { + return nil, fmt.Errorf("invalid NVMe PCI topology") + } + ctrl := nvme.NewController(disk) + ctrl.Attach(v, v) + dev := newHVFNVMePCIDevice(device, bar, uint8(interrupt-32), ctrl) + dev.IRQLine = uint8(interrupt) + h := newHVFPCIHost(dev) + h.configBase, h.configSize, h.mmioBase, h.mmioSize = configBase, configSize, windowBase, windowSize + return h, nil +} + +// AddInputPCI attaches an input function to an existing PCI host before Run. +func (v *VM) AddInputPCI(bus interface{}, device uint8, bar uint64, interrupt uint32, pointer bool, width, height uint32) (*virtio.Input, error) { + h, ok := bus.(*hvfPCIHost) + if !ok || device > 31 || h.deviceAt(0, device, 0) != nil || interrupt < 32 || interrupt > 255 || bar&0x3fff != 0 || bar < h.mmioBase || bar+0x4000 > h.mmioBase+h.mmioSize { + return nil, fmt.Errorf("invalid input PCI topology") + } + for _, d := range h.devices { + if bar < d.MMIOBAR+d.MMIOSize && d.MMIOBAR < bar+0x4000 { + return nil, fmt.Errorf("overlapping input BAR") + } + } + i := virtio.NewKeyboardInput(0, 0, interrupt-32) + if pointer { + i = virtio.NewAbsolutePointerInput(0, 0, interrupt-32, width, height) + } + i.Attach(v, v) + transport := &virtio.InputPCI{Input: i} + h.devices = append(h.devices, &hvfPCIDevice{Device: device, VendorID: 0x1af4, DeviceID: 0x1052, SubsystemID: 0x1100, Class: 9, Subclass: 2, Revision: 1, IRQLine: uint8(interrupt), IRQPin: 1, MMIOBAR: bar, MMIOSize: 0x4000, barValue: uint32(bar), mmio: transport, configure: transport.PCIConfig}) + return i, nil +} diff --git a/internal/hv/hvf/pci_darwin_arm64.go b/internal/hv/hvf/pci_darwin_arm64.go index d6b97da..d6251e6 100644 --- a/internal/hv/hvf/pci_darwin_arm64.go +++ b/internal/hv/hvf/pci_darwin_arm64.go @@ -44,7 +44,9 @@ type hvfPCIDevice struct { ReadMMIO(offset uint64, size int) (uint64, error) WriteMMIO(offset uint64, size int, value uint64) error } + configure func([]byte) barProbeValue bool + barValue uint32 } func newHVFPCIHost(devices ...*hvfPCIDevice) *hvfPCIHost { @@ -75,6 +77,7 @@ func newHVFNVMePCIDevice(dev uint8, mmioBase uint64, irq uint8, ctrl *nvme.Contr IRQPin: 1, MMIOBAR: mmioBase, MMIOSize: nvme.MMIOSize, + barValue: uint32(mmioBase), mmio: ctrl, } } @@ -177,6 +180,9 @@ func (h *hvfPCIHost) writeConfig(offset uint64, size int, value uint64) { for i := 0; i < size; i++ { dev.writeConfigByte(uint16(int(reg)+i), byte(value>>(8*i))) } + if reg < 0x14 && int(reg)+size > 0x10 && !dev.barProbeValue { + dev.MMIOBAR = uint64(dev.barValue) & ^(dev.MMIOSize - 1) + } } func (h *hvfPCIHost) deviceAt(bus, device, function uint8) *hvfPCIDevice { @@ -218,6 +224,9 @@ func (d *hvfPCIDevice) buildConfig(cfg []byte) { binary.LittleEndian.PutUint16(cfg[0x2e:0x30], d.SubsystemID) cfg[0x3c] = d.IRQLine cfg[0x3d] = d.IRQPin + if d.configure != nil { + d.configure(cfg) + } } func (d *hvfPCIDevice) deviceTreeNode() fdt.Node { @@ -238,21 +247,9 @@ func (d *hvfPCIDevice) writeConfigByte(offset uint16, value byte) { case 0x05: d.Command = (d.Command & 0x00ff) | uint16(value)<<8 case 0x10, 0x11, 0x12, 0x13: - if value == 0xff { - d.barProbeValue = true - return - } - d.barProbeValue = false - switch offset { - case 0x10: - d.MMIOBAR = (d.MMIOBAR & 0xffffffffffffff00) | uint64(value&0xf0) - case 0x11: - d.MMIOBAR = (d.MMIOBAR & 0xffffffffffff00ff) | uint64(value)<<8 - case 0x12: - d.MMIOBAR = (d.MMIOBAR & 0xffffffffff00ffff) | uint64(value)<<16 - case 0x13: - d.MMIOBAR = (d.MMIOBAR & 0xffffffff00ffffff) | uint64(value)<<24 - } + shift := (offset - 0x10) * 8 + d.barValue = d.barValue&^(0xff<= 32 { + return fmt.Errorf("invalid SIMD register %d", index) + } + vectorBinding.once.Do(func() { + vectorBinding.target, vectorBinding.err = purego.Dlsym(hvLib, "hv_vcpu_set_simd_fp_reg") + if vectorBinding.err == nil { + purego.RegisterFunc(&vectorBinding.set, vectorTrampolineAddr()) + } + }) + if vectorBinding.err != nil { + return vectorBinding.err + } + var result Return + if err := v.callOnThread(func() { + result = vectorBinding.set(v.vcpus[0].vcpu, uint32(index), &value, vectorBinding.target) + }); err != nil { + return err + } + if result != hvSuccess { + return fmt.Errorf("set SIMD register %d: %w", index, result) + } + return nil +} diff --git a/internal/hv/hvf/vector_darwin_arm64.s b/internal/hv/hvf/vector_darwin_arm64.s new file mode 100644 index 0000000..0fb3976 --- /dev/null +++ b/internal/hv/hvf/vector_darwin_arm64.s @@ -0,0 +1,10 @@ +#include "textflag.h" + +TEXT vectorTrampoline<>(SB), NOSPLIT|NOFRAME, $0 + VLD1 (R2), [V0.B16] + JMP (R3) + +TEXT ·vectorTrampolineAddr(SB), NOSPLIT, $0-8 + MOVD $vectorTrampoline<>(SB), R0 + MOVD R0, ret+0(FP) + RET diff --git a/internal/virtio/input.go b/internal/virtio/input.go index b340965..b8c2188 100644 --- a/internal/virtio/input.go +++ b/internal/virtio/input.go @@ -34,9 +34,13 @@ const ( inputRelHWheelHiRes = 0x0c inputAbsX = 0 inputAbsY = 1 - inputBtnLeft = 0x110 - inputBtnRight = 0x111 - inputBtnMiddle = 0x112 + // Match the positive 16-bit physical axis range used by the Windows + // VirtIO HID mouse. Keeping logical and physical ranges equal also avoids + // signed HID scaling across bit 15 at the midpoint of a 0xffff range. + inputAbsMaximum = 0x7fff + inputBtnLeft = 0x110 + inputBtnRight = 0x111 + inputBtnMiddle = 0x112 ) type InputKind uint8 @@ -111,6 +115,22 @@ func (i *Input) IRQAsserted() bool { return i.irqHigh } +// Stats is a bounded observation of the guest's input transport state. +func (i *Input) Stats() map[string]uint64 { + i.mu.Lock() + defer i.mu.Unlock() + ready := uint64(0) + if i.queues[0].ready { + ready = 1 + } + return map[string]uint64{ + "status": uint64(i.status), "event_queue_ready": ready, + "event_queue_size": uint64(i.queues[0].size), + "used_events": uint64(i.queues[0].usedIdx), "pending_events": uint64(len(i.pending)), + "interrupt_status": uint64(i.interruptStatus), + } +} + func (i *Input) Contains(addr uint64, size int) bool { if size <= 0 || addr < i.Base { return false @@ -380,7 +400,7 @@ func scaleAbsolutePosition(position, extent uint32) uint32 { if position >= extent { position = extent - 1 } - return position * 0xffff / (extent - 1) + return position * inputAbsMaximum / (extent - 1) } func (i *Input) SetDimensions(width, height uint32) { @@ -493,7 +513,7 @@ func (i *Input) configBytesLocked() []byte { case inputConfigABSInfo: if i.Kind == InputAbsolutePointer && (i.configSubsel == inputAbsX || i.configSubsel == inputAbsY) { data = make([]byte, 20) - binary.LittleEndian.PutUint32(data[4:8], 0xffff) + binary.LittleEndian.PutUint32(data[4:8], inputAbsMaximum) } } if len(data) > 128 { diff --git a/internal/virtio/input_pci.go b/internal/virtio/input_pci.go new file mode 100644 index 0000000..52a0817 --- /dev/null +++ b/internal/virtio/input_pci.go @@ -0,0 +1,166 @@ +package virtio + +import ( + "encoding/binary" + "fmt" +) + +// InputPCI is the modern virtio PCI transport for an Input device. BAR0 contains +// common configuration (0), notification (0x1000), ISR (0x2000), and device +// configuration (0x3000). It uses INTx; MSI-X is deliberately not advertised. +type InputPCI struct{ *Input } + +func (p *InputPCI) PCIConfig(cfg []byte) { + cfg[6] |= 0x10 + cfg[0x34] = 0x40 + for n, typ := range []byte{1, 2, 3, 4} { + a := 0x40 + n*20 + cfg[a] = 9 + if n < 3 { + cfg[a+1] = byte(a + 20) + } + cfg[a+2] = 16 + cfg[a+3] = typ + binary.LittleEndian.PutUint32(cfg[a+8:], uint32(n)*0x1000) + length := uint32(0x1000) + if n == 0 { + length = 56 + } + if n == 2 { + length = 1 + } + if n == 3 { + length = 136 + } + binary.LittleEndian.PutUint32(cfg[a+12:], length) + if typ == 2 { + cfg[a+2] = 20 + binary.LittleEndian.PutUint32(cfg[a+16:], 4) + } + } +} + +func (p *InputPCI) ReadMMIO(offset uint64, size int) (uint64, error) { + i := p.Input + i.mu.Lock() + defer i.mu.Unlock() + if size < 1 || size > 8 || offset+uint64(size) > 0x4000 { + return 0, fmt.Errorf("invalid input PCI read") + } + if offset == 0x2000 { + value := i.interruptStatus + i.interruptStatus = 0 + return uint64(value), i.updateIRQLocked() + } + if offset >= 0x3000 && offset+uint64(size) <= 0x3000+136 { + return readConfigValue(i.configBytesLocked()[offset-0x3000:], size), nil + } + var c [56]byte + binary.LittleEndian.PutUint32(c[0:], i.deviceFeatureSel) + if i.deviceFeatureSel == 1 { + binary.LittleEndian.PutUint32(c[4:], 1) + } + binary.LittleEndian.PutUint32(c[8:], i.driverFeatureSel) + if i.driverFeatureSel < 2 { + binary.LittleEndian.PutUint32(c[12:], uint32(i.driverFeatures>>(32*i.driverFeatureSel))) + } + binary.LittleEndian.PutUint16(c[16:], 0xffff) + binary.LittleEndian.PutUint16(c[18:], 2) + c[20] = byte(i.status) + c[21] = byte(i.configGeneration) + binary.LittleEndian.PutUint16(c[22:], uint16(i.queueSel)) + binary.LittleEndian.PutUint16(c[26:], 0xffff) + if q := i.selectedQueueLocked(); q != nil { + n := q.size + if n == 0 { + n = 64 + } + binary.LittleEndian.PutUint16(c[24:], n) + if q.ready { + binary.LittleEndian.PutUint16(c[28:], 1) + } + binary.LittleEndian.PutUint16(c[30:], uint16(i.queueSel)) + binary.LittleEndian.PutUint64(c[32:], q.descAddr) + binary.LittleEndian.PutUint64(c[40:], q.availAddr) + binary.LittleEndian.PutUint64(c[48:], q.usedAddr) + } + if offset+uint64(size) <= 56 { + return readConfigValue(c[offset:], size), nil + } + return 0, nil +} +func (p *InputPCI) WriteMMIO(offset uint64, size int, value uint64) error { + i := p.Input + i.mu.Lock() + defer i.mu.Unlock() + if size < 1 || size > 8 || offset+uint64(size) > 0x4000 { + return fmt.Errorf("invalid input PCI write") + } + if offset >= 0x1000 && offset < 0x2000 { + switch uint16(value) { + case 0: + return i.flushEventsLocked() + case 1: + return i.processStatusLocked() + } + return nil + } + if offset >= 0x3000 && offset < 0x3002 { + if offset == 0x3000 { + i.configSelect = byte(value) + } + if offset+uint64(size) > 0x3001 { + i.configSubsel = byte(value >> ((0x3001 - offset) * 8)) + } + return nil + } + switch offset { + case 0: + i.deviceFeatureSel = uint32(value) + case 8: + i.driverFeatureSel = uint32(value) + case 12: + if i.driverFeatureSel < 2 { + s := i.driverFeatureSel * 32 + i.driverFeatures = i.driverFeatures&^(uint64(0xffffffff)< 64 || value&(value-1) != 0 { + return fmt.Errorf("invalid input queue size %d", value) + } + q.size = uint16(value) + } + case 28: + if q := i.selectedQueueLocked(); q != nil { + q.ready = value != 0 + } + default: + if offset >= 32 && offset+uint64(size) <= 56 { + if q := i.selectedQueueLocked(); q != nil { + targets := []*uint64{&q.descAddr, &q.availAddr, &q.usedAddr} + idx := (offset - 32) / 8 + s := (offset - 32) % 8 * 8 + if offset%8+uint64(size) > 8 { + return fmt.Errorf("crossing input queue address write") + } + mask := ^uint64(0) + if size < 8 { + mask = (uint64(1) << (size * 8)) - 1 + } + *targets[idx] = *targets[idx]&^(mask<