From 4fd3abaf25bf49ba4e97ea38544cb42df134e093 Mon Sep 17 00:00:00 2001 From: flamingcow Date: Tue, 4 Aug 2026 13:21:50 -0700 Subject: [PATCH] Move the shared drm ioctl abi into internal/drm --- fb.go | 292 +++++++++++++++----------------------------- harness/grab.go | 151 ++++++----------------- internal/drm/drm.go | 145 ++++++++++++++++++++++ 3 files changed, 280 insertions(+), 308 deletions(-) create mode 100644 internal/drm/drm.go diff --git a/fb.go b/fb.go index d278fdb..3c6b832 100644 --- a/fb.go +++ b/fb.go @@ -8,116 +8,21 @@ import ( "unsafe" "golang.org/x/sys/unix" + + "g.fc.run/theater/cabletest/internal/drm" ) // Drawing lands in memory and is blitted to a buffer the display is not // reading, then swapped in whole at a vertical blank. Writing into the live // scanout buffer instead, as fbdev invites, races the beam: the blit takes a // few hundred microseconds and the display reads half of each frame. -const ( - drmIoctlBase = 0x64 - - drmModeConnected = 1 - drmModeTypePreferred = 1 << 3 - drmModePageFlipEvent = 0x01 - drmEventFlipComplete = 0x02 - - // What ADDFB means by 32 bits per pixel and 24 bits of colour. - xrgbRedShift = 16 - xrgbGreenShift = 8 - xrgbBlueShift = 0 - - // Two would be enough to stop the panel tearing, since one buffer being - // displayed while the other is drawn into is all double buffering means. - // More than two is for anything reading a frame back out: they are cycled - // in order, so a buffer is left alone for the three frames between going on - // screen and coming round again, and reading one out of uncached scanout - // memory takes a good fraction of a frame. - scanoutBuffers = 4 -) - -func drmIO(nr uintptr) uintptr { return drmIoctlBase<<8 | nr } -func drmIOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | drmIoctlBase<<8 | nr } - -type drmModeInfo struct { - clock uint32 - hdisplay, hsyncStart, hsyncEnd, htotal, hskew uint16 - vdisplay, vsyncStart, vsyncEnd, vtotal, vscan uint16 - vrefresh, flags, typ uint32 - name [32]byte -} - -type drmModeCardRes struct { - fbIDPtr, crtcIDPtr, connIDPtr, encIDPtr uint64 - countFBs, countCRTCs, countConns, countEncs uint32 - minWidth, maxWidth, minHeight, maxHeight uint32 -} - -type drmModeGetConnector struct { - encodersPtr, modesPtr, propsPtr, propValuesPtr uint64 - countModes, countProps, countEncoders uint32 - encoderID, connectorID, connectorType uint32 - connectorTypeID, connection uint32 - mmWidth, mmHeight, subpixel, pad uint32 -} - -type drmModeGetEncoder struct { - encoderID, encoderType uint32 - crtcID uint32 - possibleCRTCs, possibleClones uint32 -} - -type drmModeCrtc struct { - setConnectorsPtr uint64 - countConnectors uint32 - crtcID uint32 - fbID uint32 - x, y uint32 - gammaSize uint32 - modeValid uint32 - mode drmModeInfo -} - -type drmModeFBCmd struct { - fbID, width, height, pitch, bpp, depth uint32 - handle uint32 -} - -type drmModeCreateDumb struct { - height, width, bpp, flags uint32 - handle, pitch uint32 - size uint64 -} - -type drmModeMapDumb struct { - handle, pad uint32 - offset uint64 -} - -type drmModeCrtcPageFlip struct { - crtcID, fbID, flags, reserved uint32 - userData uint64 -} - -var ( - drmSetMaster = drmIO(0x1e) - drmDropMaster = drmIO(0x1f) - drmGetResources = drmIOWR(0xa0, unsafe.Sizeof(drmModeCardRes{})) - drmSetCrtc = drmIOWR(0xa2, unsafe.Sizeof(drmModeCrtc{})) - drmGetEncoder = drmIOWR(0xa6, unsafe.Sizeof(drmModeGetEncoder{})) - drmGetConnector = drmIOWR(0xa7, unsafe.Sizeof(drmModeGetConnector{})) - drmAddFB = drmIOWR(0xae, unsafe.Sizeof(drmModeFBCmd{})) - drmPageFlip = drmIOWR(0xb0, unsafe.Sizeof(drmModeCrtcPageFlip{})) - drmCreateDumb = drmIOWR(0xb2, unsafe.Sizeof(drmModeCreateDumb{})) - drmMapDumb = drmIOWR(0xb3, unsafe.Sizeof(drmModeMapDumb{})) -) - -func drmIoctl(fd int, req uintptr, arg unsafe.Pointer) error { - if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)); errno != 0 { - return errno - } - return nil -} +// +// Two buffers would be enough to stop the panel tearing. More than two is for +// anything reading a frame back out: they are cycled in order, so a buffer is +// left alone for the three frames between going on screen and coming round +// again, and reading one out of uncached scanout memory takes a good fraction +// of a frame. +const scanoutBuffers = 4 type scanout struct { fbID uint32 @@ -125,8 +30,7 @@ type scanout struct { } // w and h are the logical canvas, which is portrait; pw and ph are the panel, -// which is landscape. Every draw is turned a quarter turn on its way to memory, -// so logical top lands on the panel's right edge. +// which is landscape. type framebuffer struct { fd int back []byte @@ -147,7 +51,7 @@ type framebuffer struct { } func (fb *framebuffer) offset(x, y int) int { - return x*fb.stride + (fb.pw-1-y)*4 + return drm.Offset(x, y, fb.stride, fb.pw) } func (fb *framebuffer) fromPanel(x, y int) (int, int) { @@ -155,20 +59,20 @@ func (fb *framebuffer) fromPanel(x, y int) (int, int) { } func cardResources(fd int) (crtcs, conns []uint32, err error) { - var res drmModeCardRes - if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil { + var res drm.ModeCardRes + if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil { return nil, nil, fmt.Errorf("get resources: %w", err) } - if res.countCRTCs == 0 || res.countConns == 0 { + if res.CountCRTCs == 0 || res.CountConns == 0 { return nil, nil, fmt.Errorf("card has no crtcs or connectors") } - crtcs = make([]uint32, res.countCRTCs) - conns = make([]uint32, res.countConns) - res.countFBs, res.countEncs = 0, 0 - res.fbIDPtr, res.encIDPtr = 0, 0 - res.crtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0]))) - res.connIDPtr = uint64(uintptr(unsafe.Pointer(&conns[0]))) - if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil { + crtcs = make([]uint32, res.CountCRTCs) + conns = make([]uint32, res.CountConns) + res.CountFBs, res.CountEncs = 0, 0 + res.FBIDPtr, res.EncIDPtr = 0, 0 + res.CrtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0]))) + res.ConnIDPtr = uint64(uintptr(unsafe.Pointer(&conns[0]))) + if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil { return nil, nil, fmt.Errorf("get resources: %w", err) } return crtcs, conns, nil @@ -176,66 +80,66 @@ func cardResources(fd int) (crtcs, conns []uint32, err error) { // The preferred mode is the panel's native one; anything else would be the // driver scaling a wrong-sized image onto it. -func preferredMode(fd int, connID uint32) (drmModeInfo, error) { - c := drmModeGetConnector{connectorID: connID} - if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&c)); err != nil { - return drmModeInfo{}, err +func preferredMode(fd int, connID uint32) (drm.ModeInfo, error) { + c := drm.ModeGetConnector{ConnectorID: connID} + if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&c)); err != nil { + return drm.ModeInfo{}, err } - if c.countModes == 0 { - return drmModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) + if c.CountModes == 0 { + return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) } - modes := make([]drmModeInfo, c.countModes) - q := drmModeGetConnector{ - connectorID: connID, - countModes: c.countModes, - modesPtr: uint64(uintptr(unsafe.Pointer(&modes[0]))), + modes := make([]drm.ModeInfo, c.CountModes) + q := drm.ModeGetConnector{ + ConnectorID: connID, + CountModes: c.CountModes, + ModesPtr: uint64(uintptr(unsafe.Pointer(&modes[0]))), } - if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&q)); err != nil { - return drmModeInfo{}, err + if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err != nil { + return drm.ModeInfo{}, err } - if q.countModes == 0 { - return drmModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) + if q.CountModes == 0 { + return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) } - for _, m := range modes[:q.countModes] { - if m.typ&drmModeTypePreferred != 0 { + for _, m := range modes[:q.CountModes] { + if m.Type&drm.TypePreferred != 0 { return m, nil } } return modes[0], nil } -func crtcFor(fd int, c drmModeGetConnector, crtcs []uint32) (uint32, error) { - encoders := []uint32{c.encoderID} - if c.countEncoders > 0 { - list := make([]uint32, c.countEncoders) - q := drmModeGetConnector{ - connectorID: c.connectorID, - countEncoders: c.countEncoders, - encodersPtr: uint64(uintptr(unsafe.Pointer(&list[0]))), +func crtcFor(fd int, c drm.ModeGetConnector, crtcs []uint32) (uint32, error) { + encoders := []uint32{c.EncoderID} + if c.CountEncoders > 0 { + list := make([]uint32, c.CountEncoders) + q := drm.ModeGetConnector{ + ConnectorID: c.ConnectorID, + CountEncoders: c.CountEncoders, + EncodersPtr: uint64(uintptr(unsafe.Pointer(&list[0]))), } - if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&q)); err == nil { - encoders = append(encoders, list[:q.countEncoders]...) + if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err == nil { + encoders = append(encoders, list[:q.CountEncoders]...) } } for _, id := range encoders { if id == 0 { continue } - e := drmModeGetEncoder{encoderID: id} - if err := drmIoctl(fd, drmGetEncoder, unsafe.Pointer(&e)); err != nil { + e := drm.ModeGetEncoder{EncoderID: id} + if err := drm.Ioctl(fd, drm.GetEncoder, unsafe.Pointer(&e)); err != nil { continue } // Already driving this connector, otherwise anything it can be wired to. - if e.crtcID != 0 { - return e.crtcID, nil + if e.CrtcID != 0 { + return e.CrtcID, nil } for i, crtc := range crtcs { - if e.possibleCRTCs&(1< n { return } - if typ == drmEventFlipComplete { + if typ == drm.EventFlipComplete { select { case fb.flips <- struct{}{}: default: @@ -402,20 +306,18 @@ func (fb *framebuffer) close() { // Dropping master hands the display back to the kernel console, which // restores its own mode. The framebuffers and dumb buffers are reclaimed // when the last reference to the fd goes. - drmIoctl(fb.fd, drmDropMaster, nil) + drm.Ioctl(fb.fd, drm.DropMaster, nil) unix.Close(fb.fd) } -func (fb *framebuffer) pixel(c rgb) uint32 { - return uint32(c.r)<= x1 || y0 >= y1 { return } - v := fb.pixel(c) + v := pixel(c) span := make([]byte, (y1-y0)*4) for i := 0; i+4 <= len(span); i += 4 { span[i+0] = byte(v) @@ -480,7 +382,7 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { } o := fb.offset(x, y) if cov == 255 { - v := fb.pixel(c) + v := pixel(c) fb.back[o+0] = byte(v) fb.back[o+1] = byte(v >> 8) fb.back[o+2] = byte(v >> 16) @@ -490,15 +392,13 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { a := uint32(cov) old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 | uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24 - orr := uint8(old >> xrgbRedShift) - og := uint8(old >> xrgbGreenShift) - ob := uint8(old >> xrgbBlueShift) + orr, og, ob := drm.Unpack(old) mix := rgb{ r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255), g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255), b: uint8((uint32(c.b)*a + uint32(ob)*(255-a)) / 255), } - v := fb.pixel(mix) + v := pixel(mix) fb.back[o+0] = byte(v) fb.back[o+1] = byte(v >> 8) fb.back[o+2] = byte(v >> 16) @@ -511,12 +411,12 @@ func (fb *framebuffer) flush() error { next := (fb.front + 1) % scanoutBuffers copy(fb.bufs[next].mem, fb.back) - flip := drmModeCrtcPageFlip{ - crtcID: fb.crtcID, - fbID: fb.bufs[next].fbID, - flags: drmModePageFlipEvent, + flip := drm.ModeCrtcPageFlip{ + CrtcID: fb.crtcID, + FBID: fb.bufs[next].fbID, + Flags: drm.PageFlipEvent, } - if err := drmIoctl(fb.fd, drmPageFlip, unsafe.Pointer(&flip)); err != nil { + if err := drm.Ioctl(fb.fd, drm.PageFlip, unsafe.Pointer(&flip)); err != nil { return fmt.Errorf("page flip: %w", err) } fb.front = next diff --git a/harness/grab.go b/harness/grab.go index 3e539a5..b65a6f3 100644 --- a/harness/grab.go +++ b/harness/grab.go @@ -9,83 +9,15 @@ import ( "unsafe" "golang.org/x/sys/unix" + + "g.fc.run/theater/cabletest/internal/drm" ) // cabletest drives the panel through drm, so /dev/fb0 holds the kernel console // and is not worth reading. The displayed buffer is reachable from out here // instead: GETCRTC names it, and root is allowed a handle to it without being // drm master, so it can be read without the owning process cooperating. -const ( - drmCardGlob = "/dev/dri/card*" - - drmVblankRelative = 0x1 - drmVblankHighCrtcMask = 0x3e - drmVblankHighCrtcShft = 1 - - xrgbRedShift = 16 - xrgbGreenShift = 8 - xrgbBlueShift = 0 -) - -func drmIOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | 0x64<<8 | nr } - -type drmModeInfo struct { - clock uint32 - hdisplay, hsyncStart, hsyncEnd, htotal, hskew uint16 - vdisplay, vsyncStart, vsyncEnd, vtotal, vscan uint16 - vrefresh, flags, typ uint32 - name [32]byte -} - -type drmModeCardRes struct { - fbIDPtr, crtcIDPtr, connIDPtr, encIDPtr uint64 - countFBs, countCRTCs, countConns, countEncs uint32 - minWidth, maxWidth, minHeight, maxHeight uint32 -} - -type drmModeCrtc struct { - setConnectorsPtr uint64 - countConnectors uint32 - crtcID uint32 - fbID uint32 - x, y uint32 - gammaSize uint32 - modeValid uint32 - mode drmModeInfo -} - -type drmModeFBCmd struct { - fbID, width, height, pitch, bpp, depth uint32 - handle uint32 -} - -type drmModeMapDumb struct { - handle, pad uint32 - offset uint64 -} - -type drmWaitVblank struct { - typ uint32 - sequence uint32 - signal uint64 - tvSec int64 - tvUsec int64 -} - -var ( - drmGetResources = drmIOWR(0xa0, unsafe.Sizeof(drmModeCardRes{})) - drmGetCrtc = drmIOWR(0xa1, unsafe.Sizeof(drmModeCrtc{})) - drmGetFB = drmIOWR(0xad, unsafe.Sizeof(drmModeFBCmd{})) - drmMapDumb = drmIOWR(0xb3, unsafe.Sizeof(drmModeMapDumb{})) - drmWaitVblankIO = drmIOWR(0x3a, 24) -) - -func drmIoctl(fd int, req uintptr, arg unsafe.Pointer) error { - if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)); errno != 0 { - return errno - } - return nil -} +const drmCardGlob = "/dev/dri/card*" type grabber struct { fd int @@ -113,26 +45,26 @@ func openGrabber() (*grabber, error) { } func activeCrtc(fd int) (uint32, int, error) { - var res drmModeCardRes - if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil { + var res drm.ModeCardRes + if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil { return 0, 0, err } - if res.countCRTCs == 0 { + if res.CountCRTCs == 0 { return 0, 0, fmt.Errorf("card has no crtcs") } - crtcs := make([]uint32, res.countCRTCs) - res.countFBs, res.countConns, res.countEncs = 0, 0, 0 - res.fbIDPtr, res.connIDPtr, res.encIDPtr = 0, 0, 0 - res.crtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0]))) - if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil { + crtcs := make([]uint32, res.CountCRTCs) + res.CountFBs, res.CountConns, res.CountEncs = 0, 0, 0 + res.FBIDPtr, res.ConnIDPtr, res.EncIDPtr = 0, 0, 0 + res.CrtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0]))) + if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil { return 0, 0, err } for i, id := range crtcs { - c := drmModeCrtc{crtcID: id} - if err := drmIoctl(fd, drmGetCrtc, unsafe.Pointer(&c)); err != nil { + c := drm.ModeCrtc{CrtcID: id} + if err := drm.Ioctl(fd, drm.GetCrtc, unsafe.Pointer(&c)); err != nil { continue } - if c.fbID != 0 && c.modeValid != 0 { + if c.FBID != 0 && c.ModeValid != 0 { return id, i, nil } } @@ -145,12 +77,12 @@ func (g *grabber) close() { unix.Close(g.fd) } // the flip cycle decides whether it stays ahead of the writer. Woken at a // blank, the buffer GETCRTC then names has just gone on screen. func (g *grabber) waitVblank() error { - v := drmWaitVblank{ - typ: drmVblankRelative | uint32(g.index<> xrgbRedShift), - G: uint8(v >> xrgbGreenShift), - B: uint8(v >> xrgbBlueShift), - A: 255, - }) + r, g, b := drm.Unpack(v) + img.SetNRGBA(x, y, color.NRGBA{R: r, G: g, B: b, A: 255}) } } return img diff --git a/internal/drm/drm.go b/internal/drm/drm.go new file mode 100644 index 0000000..4baddd7 --- /dev/null +++ b/internal/drm/drm.go @@ -0,0 +1,145 @@ +// Package drm is the kernel mode-setting ABI, shared by the panel this draws on +// and the grabber that reads it back out. These structs are a layout contract, +// so a field added or reordered in one copy and not the other would silently +// misread the kernel: there is deliberately only one copy. +package drm + +import ( + "unsafe" + + "golang.org/x/sys/unix" +) + +const ioctlBase = 0x64 + +func IO(nr uintptr) uintptr { return ioctlBase<<8 | nr } +func IOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | ioctlBase<<8 | nr } + +func Ioctl(fd int, req uintptr, arg unsafe.Pointer) error { + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)); errno != 0 { + return errno + } + return nil +} + +const ( + Connected = 1 + TypePreferred = 1 << 3 + PageFlipEvent = 0x01 + EventFlipComplete = 0x02 + + VblankRelative = 0x1 + VblankHighCrtcMask = 0x3e + VblankHighCrtcShift = 1 +) + +type ModeInfo struct { + Clock uint32 + Hdisplay, HsyncStart, HsyncEnd, Htotal, Hskew uint16 + Vdisplay, VsyncStart, VsyncEnd, Vtotal, Vscan uint16 + Vrefresh, Flags, Type uint32 + Name [32]byte +} + +type ModeCardRes struct { + FBIDPtr, CrtcIDPtr, ConnIDPtr, EncIDPtr uint64 + CountFBs, CountCRTCs, CountConns, CountEncs uint32 + MinWidth, MaxWidth, MinHeight, MaxHeight uint32 +} + +type ModeGetConnector struct { + EncodersPtr, ModesPtr, PropsPtr, PropValuesPtr uint64 + CountModes, CountProps, CountEncoders uint32 + EncoderID, ConnectorID, ConnectorType uint32 + ConnectorTypeID, Connection uint32 + MmWidth, MmHeight, Subpixel, Pad uint32 +} + +type ModeGetEncoder struct { + EncoderID, EncoderType uint32 + CrtcID uint32 + PossibleCRTCs, PossibleClones uint32 +} + +type ModeCrtc struct { + SetConnectorsPtr uint64 + CountConnectors uint32 + CrtcID uint32 + FBID uint32 + X, Y uint32 + GammaSize uint32 + ModeValid uint32 + Mode ModeInfo +} + +type ModeFBCmd struct { + FBID, Width, Height, Pitch, Bpp, Depth uint32 + Handle uint32 +} + +type ModeCreateDumb struct { + Height, Width, Bpp, Flags uint32 + Handle, Pitch uint32 + Size uint64 +} + +type ModeMapDumb struct { + Handle, Pad uint32 + Offset uint64 +} + +type ModeCrtcPageFlip struct { + CrtcID, FBID, Flags, Reserved uint32 + UserData uint64 +} + +// WaitVblank has only Type and Sequence ever set, and those sit at the same +// offsets in both arms of the kernel's union. The request below carries the +// union's own size rather than this struct's, which is larger. +type WaitVblank struct { + Type uint32 + Sequence uint32 + Signal uint64 + TvSec int64 + TvUsec int64 +} + +const waitVblankUnionSize = 24 + +var ( + SetMaster = IO(0x1e) + DropMaster = IO(0x1f) + WaitVblankIO = IOWR(0x3a, waitVblankUnionSize) + GetResources = IOWR(0xa0, unsafe.Sizeof(ModeCardRes{})) + GetCrtc = IOWR(0xa1, unsafe.Sizeof(ModeCrtc{})) + SetCrtc = IOWR(0xa2, unsafe.Sizeof(ModeCrtc{})) + GetEncoder = IOWR(0xa6, unsafe.Sizeof(ModeGetEncoder{})) + GetConnector = IOWR(0xa7, unsafe.Sizeof(ModeGetConnector{})) + GetFB = IOWR(0xad, unsafe.Sizeof(ModeFBCmd{})) + AddFB = IOWR(0xae, unsafe.Sizeof(ModeFBCmd{})) + PageFlip = IOWR(0xb0, unsafe.Sizeof(ModeCrtcPageFlip{})) + CreateDumb = IOWR(0xb2, unsafe.Sizeof(ModeCreateDumb{})) + MapDumb = IOWR(0xb3, unsafe.Sizeof(ModeMapDumb{})) +) + +// What ADDFB means by 32 bits per pixel and 24 bits of colour. +const ( + redShift = 16 + greenShift = 8 + blueShift = 0 +) + +func Pack(r, g, b uint8) uint32 { + return uint32(r)<> redShift), uint8(v >> greenShift), uint8(v >> blueShift) +} + +// Offset turns the quarter turn between the portrait canvas and the landscape +// panel, putting logical top on the panel's right edge. Drawing and reading +// back have to agree on this exactly. +func Offset(x, y, stride, panelW int) int { + return x*stride + (panelW-1-y)*4 +}