230 lines
6.4 KiB
Go
230 lines
6.4 KiB
Go
package rdpgfx
|
|
|
|
import (
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
// TestParallelRowsCoverage verifies parallelRows invokes fn over contiguous,
|
|
// non-overlapping chunks that together cover [0,h) exactly once, for both the
|
|
// serial (small) and parallel (large) regimes.
|
|
func TestParallelRowsCoverage(t *testing.T) {
|
|
for _, dim := range []struct{ w, h int }{
|
|
{16, 16}, // tiny → serial
|
|
{64, 64}, // small → serial
|
|
{256, 256}, // at threshold → parallel
|
|
{512, 300}, // parallel, height not divisible by worker count
|
|
{1920, 1080}, // typical full-screen → parallel
|
|
{8, 1}, // single row
|
|
{8, 0}, // zero rows
|
|
} {
|
|
counts := make([]int32, dim.h)
|
|
parallelRows(dim.w, dim.h, func(y0, y1 int) {
|
|
for r := y0; r < y1; r++ {
|
|
counts[r]++
|
|
}
|
|
})
|
|
for r := 0; r < dim.h; r++ {
|
|
if counts[r] != 1 {
|
|
t.Fatalf("dim %dx%d: row %d visited %d times, want 1", dim.w, dim.h, r, counts[r])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// serialI420ToBGRA is an independent reference implementation used to verify the
|
|
// parallelised i420ToBGRA output is identical regardless of how rows are split.
|
|
func serialI420ToBGRA(src *H264FrameI420) []byte {
|
|
w, h := src.Width, src.Height
|
|
out := make([]byte, w*h*4)
|
|
for row := 0; row < h; row++ {
|
|
yOff := row * src.YStride
|
|
uOff := (row >> 1) * src.UStride
|
|
vOff := (row >> 1) * src.VStride
|
|
for col := 0; col < w; col++ {
|
|
uv := col >> 1
|
|
o := (row*w + col) * 4
|
|
u := int(src.U[uOff+uv]) - 128
|
|
v := int(src.V[vOff+uv]) - 128
|
|
if src.FullRange {
|
|
y := int(src.Y[yOff+col])
|
|
out[o] = clampByte((256*y + 475*u + 128) >> 8)
|
|
out[o+1] = clampByte((256*y - 48*u - 120*v + 128) >> 8)
|
|
out[o+2] = clampByte((256*y + 403*v + 128) >> 8)
|
|
} else {
|
|
c := int(src.Y[yOff+col]) - 16
|
|
out[o] = clampByte((298*c + 541*u + 128) >> 8)
|
|
out[o+1] = clampByte((298*c - 55*u - 136*v + 128) >> 8)
|
|
out[o+2] = clampByte((298*c + 459*v + 128) >> 8)
|
|
}
|
|
out[o+3] = 255
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func makeI420(w, h int, fullRange bool, rng *rand.Rand) *H264FrameI420 {
|
|
cw, ch := (w+1)/2, (h+1)/2
|
|
f := &H264FrameI420{
|
|
Y: make([]byte, w*h), U: make([]byte, cw*ch), V: make([]byte, cw*ch),
|
|
YStride: w, UStride: cw, VStride: cw,
|
|
Width: w, Height: h, FullRange: fullRange,
|
|
}
|
|
for i := range f.Y {
|
|
f.Y[i] = byte(rng.Intn(256))
|
|
}
|
|
for i := range f.U {
|
|
f.U[i] = byte(rng.Intn(256))
|
|
f.V[i] = byte(rng.Intn(256))
|
|
}
|
|
return f
|
|
}
|
|
|
|
// TestI420ToBGRAParallelMatchesSerial checks that the parallelised conversion
|
|
// produces byte-identical output to the reference for both small (serial) and
|
|
// large (parallel) frames, in full- and limited-range modes.
|
|
func TestI420ToBGRAParallelMatchesSerial(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(1))
|
|
for _, dim := range []struct{ w, h int }{
|
|
{64, 64}, // serial path
|
|
{640, 480}, // parallel path
|
|
{512, 511}, // parallel, odd height
|
|
} {
|
|
for _, fr := range []bool{false, true} {
|
|
f := makeI420(dim.w, dim.h, fr, rng)
|
|
got, pooled := i420ToBGRA(f)
|
|
if got == nil {
|
|
t.Fatalf("i420ToBGRA returned nil for %dx%d", dim.w, dim.h)
|
|
}
|
|
want := serialI420ToBGRA(f)
|
|
if len(got) != len(want) {
|
|
t.Fatalf("%dx%d fr=%v: len %d != %d", dim.w, dim.h, fr, len(got), len(want))
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("%dx%d fr=%v: byte %d = %d, want %d", dim.w, dim.h, fr, i, got[i], want[i])
|
|
}
|
|
}
|
|
if pooled {
|
|
releaseBitmapBuf(got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkI420ToBGRA1080p(b *testing.B) {
|
|
rng := rand.New(rand.NewSource(2))
|
|
f := makeI420(1920, 1080, false, rng)
|
|
b.SetBytes(int64(1920 * 1080 * 4))
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
out, pooled := i420ToBGRA(f)
|
|
if pooled {
|
|
releaseBitmapBuf(out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// serialCombineAVC444v2BGRA mirrors combineAVC444v2BGRA's exact indexing in a
|
|
// single serial loop, used to confirm the parallelised version is byte-identical
|
|
// regardless of how the rows are split across workers.
|
|
func serialCombineAVC444v2BGRA(yPlane []byte, yStride int, cachedU, cachedV []byte, uvStride int,
|
|
i420aux *H264FrameI420, fullRange bool, w, h int) []byte {
|
|
out := make([]byte, w*h*4)
|
|
halfW := w / 2
|
|
quarterW := w / 4
|
|
for row := 0; row < h; row++ {
|
|
yRowOff := row * yStride
|
|
uvRow := row >> 1
|
|
uvRowOff := uvRow * uvStride
|
|
auxYRowOff := row * i420aux.YStride
|
|
auxURowOff := uvRow * i420aux.UStride
|
|
auxVRowOff := uvRow * i420aux.VStride
|
|
outIdx := row * w * 4
|
|
for col := 0; col < w; col++ {
|
|
Y := yPlane[yRowOff+col]
|
|
var Cb, Cr byte
|
|
if col&1 == 1 {
|
|
k := col >> 1
|
|
Cb = i420aux.Y[auxYRowOff+k]
|
|
Cr = i420aux.Y[auxYRowOff+halfW+k]
|
|
} else if row&1 == 0 {
|
|
k := col >> 1
|
|
Cb = cachedU[uvRowOff+k]
|
|
Cr = cachedV[uvRowOff+k]
|
|
} else {
|
|
k := col >> 2
|
|
if col&2 == 0 {
|
|
Cb = i420aux.U[auxURowOff+k]
|
|
Cr = i420aux.U[auxURowOff+quarterW+k]
|
|
} else {
|
|
Cb = i420aux.V[auxVRowOff+k]
|
|
Cr = i420aux.V[auxVRowOff+quarterW+k]
|
|
}
|
|
}
|
|
u := int(Cb) - 128
|
|
v := int(Cr) - 128
|
|
if fullRange {
|
|
y := int(Y)
|
|
out[outIdx] = clampByte((256*y + 475*u + 128) >> 8)
|
|
out[outIdx+1] = clampByte((256*y - 48*u - 120*v + 128) >> 8)
|
|
out[outIdx+2] = clampByte((256*y + 403*v + 128) >> 8)
|
|
} else {
|
|
c := int(Y) - 16
|
|
out[outIdx] = clampByte((298*c + 541*u + 128) >> 8)
|
|
out[outIdx+1] = clampByte((298*c - 55*u - 136*v + 128) >> 8)
|
|
out[outIdx+2] = clampByte((298*c + 459*v + 128) >> 8)
|
|
}
|
|
out[outIdx+3] = 255
|
|
outIdx += 4
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestCombineAVC444v2BGRAParallelMatchesSerial(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(3))
|
|
for _, dim := range []struct{ w, h int }{
|
|
{64, 64}, // serial path
|
|
{640, 480}, // parallel path
|
|
{512, 510}, // parallel, even dims
|
|
} {
|
|
w, h := dim.w, dim.h
|
|
uvStride := (w + 1) / 2
|
|
uvH := (h + 1) / 2
|
|
yPlane := make([]byte, w*h)
|
|
cachedU := make([]byte, uvStride*uvH)
|
|
cachedV := make([]byte, uvStride*uvH)
|
|
for i := range yPlane {
|
|
yPlane[i] = byte(rng.Intn(256))
|
|
}
|
|
for i := range cachedU {
|
|
cachedU[i] = byte(rng.Intn(256))
|
|
cachedV[i] = byte(rng.Intn(256))
|
|
}
|
|
aux := &H264FrameI420{
|
|
Y: make([]byte, w*h), U: make([]byte, uvStride*uvH), V: make([]byte, uvStride*uvH),
|
|
YStride: w, UStride: uvStride, VStride: uvStride, Width: w, Height: h,
|
|
}
|
|
for i := range aux.Y {
|
|
aux.Y[i] = byte(rng.Intn(256))
|
|
}
|
|
for i := range aux.U {
|
|
aux.U[i] = byte(rng.Intn(256))
|
|
aux.V[i] = byte(rng.Intn(256))
|
|
}
|
|
for _, fr := range []bool{false, true} {
|
|
got, pooled := combineAVC444v2BGRA(yPlane, w, cachedU, cachedV, uvStride, aux, fr, w, h, nil)
|
|
want := serialCombineAVC444v2BGRA(yPlane, w, cachedU, cachedV, uvStride, aux, fr, w, h)
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("%dx%d fr=%v: byte %d = %d, want %d", w, h, fr, i, got[i], want[i])
|
|
}
|
|
}
|
|
if pooled {
|
|
releaseBitmapBuf(got)
|
|
}
|
|
}
|
|
}
|
|
}
|