diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 8dbbefe..9a7b2bc 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -3,17 +3,20 @@ package renderer import ( "errors" "fmt" - "mxl-player/internal/sdl" "unsafe" + "mxl-player/internal/sdl" + "github.com/christerso/vulkan-go/vk" ) -// Raises by swapchain creation, whem windos is minimized -var ErrMinimized = errors.New("window minimized") - -// ErrOutOfDate is returned by DrawFrame when the swapchain needs recreation. -var ErrOutOfDate = errors.New("swapchain out of date") +var ( + // Raises by swapchain creation, whem windos is minimized + ErrMinimized = errors.New("window minimized") + // ErrOutOfDate is returned by DrawFrame when the swapchain needs recreation. + ErrOutOfDate = errors.New("swapchain out of date") + ErrInvalidVideoFrame = errors.New("invalid video frame") +) // shader push-constants block type PushConstants struct { @@ -392,6 +395,64 @@ func (r *Renderer) RecreateBuffers(newSize vk.DeviceSize) error { return nil } +func (r *Renderer) StageFrame( + payload []byte, + width uint32, + height uint32, + stride uint32, +) error { + frameSize, err := validateFramePayload(len(payload), width, height, stride) + if err != nil { + return err + } + + if frameSize != r.FrameSize() { + // RecreateBuffers waits for the device to become idle. + if err := r.RecreateBuffers(frameSize); err != nil { + return fmt.Errorf("resize video buffers: %w", err) + } + } else { + // The previous submitted frame may still read the mapped staging buffer. + if err := r.dev.WaitFence(r.inFlight, ^uint64(0)); err != nil { + return fmt.Errorf("wait before staging video: %w", err) + } + } + + vk.CopyToMapped( + r.StagingMapped(), + payload[:int(frameSize)], + ) + return nil +} + +func validateFramePayload( + payloadLen int, + width uint32, + height uint32, + stride uint32, +) (vk.DeviceSize, error) { + if width == 0 || height == 0 || stride == 0 { + return 0, fmt.Errorf( + "%w: dimensions=%dx%d stride=%d", + ErrInvalidVideoFrame, + width, + height, + stride, + ) + } + + requiredSize := uint64(stride) * uint64(height) + if payloadLen < 0 || requiredSize > uint64(payloadLen) { + return 0, fmt.Errorf( + "%w: payload=%d required=%d", + ErrInvalidVideoFrame, + payloadLen, + requiredSize, + ) + } + return vk.DeviceSize(requiredSize), nil +} + // DrawFrame acquires an image, records commands, submits, and presents. // Returns ErrOutOfDate if the swapchain needs recreation func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error { diff --git a/internal/renderer/renderer_test.go b/internal/renderer/renderer_test.go new file mode 100644 index 0000000..3ecdcc5 --- /dev/null +++ b/internal/renderer/renderer_test.go @@ -0,0 +1,122 @@ +package renderer + +import ( + "errors" + "strconv" + "testing" + + "github.com/christerso/vulkan-go/vk" +) + +func TestValidateFramePayload(t *testing.T) { + tests := []struct { + name string + payloadLen int + width uint32 + height uint32 + stride uint32 + wantSize vk.DeviceSize + wantErr bool + }{ + { + name: "valid frame", + payloadLen: 5120 * 1080, + width: 1920, + height: 1080, + stride: 5120, + wantSize: vk.DeviceSize(5120 * 1080), + }, + { + name: "payload may be larger than frame", + payloadLen: 5120*1080 + 128, + width: 1920, + height: 1080, + stride: 5120, + wantSize: vk.DeviceSize(5120 * 1080), + }, + { + name: "zero width", + payloadLen: 100, + height: 10, + stride: 10, + wantErr: true, + }, + { + name: "zero height", + payloadLen: 100, + width: 10, + stride: 10, + wantErr: true, + }, + { + name: "zero stride", + payloadLen: 100, + width: 10, + height: 10, + wantErr: true, + }, + { + name: "payload is too small", + payloadLen: 99, + width: 10, + height: 10, + stride: 10, + wantErr: true, + }, + { + name: "negative payload length", + payloadLen: -1, + width: 10, + height: 10, + stride: 10, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateFramePayload( + tt.payloadLen, + tt.width, + tt.height, + tt.stride, + ) + + if tt.wantErr { + if !errors.Is(err, ErrInvalidVideoFrame) { + t.Fatalf("validateFramePayload() error = %v, want %v", err, ErrInvalidVideoFrame) + } + return + } + if err != nil { + t.Fatalf("validateFramePayload() error = %v, want nil", err) + } + if got != tt.wantSize { + t.Errorf("validateFramePayload() size = %d, want %d", got, tt.wantSize) + } + }) + } +} + +func TestValidateFramePayloadUses64BitSize(t *testing.T) { + if strconv.IntSize < 64 { + t.Skip("test requires a 64-bit int") + } + + stride := ^uint32(0) + height := uint32(2) + required := uint64(stride) * uint64(height) + + got, err := validateFramePayload( + int(required), + 1, + height, + stride, + ) + if err != nil { + t.Fatalf("validateFramePayload() error = %v, want nil", err) + } + if uint64(got) != required { + t.Fatalf("validateFramePayload() size = %d, want %d", got, required) + } +}