diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index 5e27429..4c95227 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "log" + "mxl-player/internal/renderer" "mxl-player/internal/sdl" "mxl-player/internal/source" "runtime" @@ -19,13 +20,11 @@ import ( const ( APP_NAME = "MXL Player" APP_VER = "0.0.1" - WIN_WIDTH int32 = 1280 - WIN_HEIGHT int32 = 720 + WIN_WIDTH int32 = 1920 + WIN_HEIGHT int32 = 1080 ) func main() { - var errMinimized = errors.New("Window minimized") - // TODO: remove flags defaults mxlDomain := flag.String("d", "/dev/shm/mxl", "MXL domain") mxlVideoFlowID := flag.String("v", "5fbec3b1-1b0f-417d-9059-8b94a47197ed", "MXL video flow UUID") flag.Parse() @@ -46,18 +45,15 @@ func main() { return } - // Vulkan init if err := vk.Load(); err != nil { panic(err) } - // Vulkan instance extensions SDL needs. sdlExtensions := sdl.VulkanGetInstanceExtensions() if len(sdlExtensions) == 0 { log.Fatal("sdlVulkanGetInstanceExtensions is 0") return } - // Vulkan Instance creation var vkLayers []string vkExtensions := append(sdlExtensions, vk.ExtDebugUtils) vkInstance, err := vk.CreateInstance(vk.InstanceConfig{ @@ -71,7 +67,7 @@ func main() { panic(err) } defer vkInstance.Destroy() - // Vulkan surface + var vkSurface uint64 if !sdl.VulkanCreateSurface(windowHandler, uintptr(vkInstance), 0, &vkSurface) { log.Fatalf("SDL_Vulkan_CreateSurface: %s", sdl.GetError()) @@ -80,7 +76,6 @@ func main() { vkSurf := vk.SurfaceKHR(vkSurface) defer vkInstance.DestroySurface(vkSurf) - // Vulkan Physical Device devices, err := vkInstance.EnumeratePhysicalDevices() if err != nil || len(devices) == 0 { panic("No Vulkan devices") @@ -90,7 +85,7 @@ func main() { fmt.Printf("%s (%s)\n", info.Name, info.Type) } vkPhysDevice := devices[0] - // Vulkan Surface & Graphics Queue + gfx, err := vkPhysDevice.GraphicsFamily() if err != nil { panic(err.Error()) @@ -99,6 +94,7 @@ func main() { log.Fatalf("graphics queue cannot present") return } + vkDevice, vkQueue, err := vkPhysDevice.CreateDevice(vk.DeviceConfig{ GraphicsFamily: gfx, Extensions: []string{"VK_KHR_swapchain"}, @@ -107,286 +103,31 @@ func main() { panic(err) } defer vkDevice.Destroy() - // TODO: colorspace & render pass - var vkFormat vk.Format = vk.FormatUndefined - var vkColorSpace uint32 - formats, _ := vkPhysDevice.SurfaceFormats(vkSurf) - for _, f := range formats { - // Prefer 10-bit RGB (A2B10G10R10_UNORM = 64) to preserve V210's 10 bits - if f.Format == vk.Format(64) && f.ColorSpace == vk.ColorSpaceSRGBNonlinear { - vkFormat = f.Format - vkColorSpace = f.ColorSpace - } - } - if vkFormat == vk.FormatUndefined { - vkFormat = formats[0].Format - vkColorSpace = formats[0].ColorSpace - } - var vkPresentMode vk.PresentMode = vk.PresentModeFIFO - modes, _ := vkPhysDevice.SurfacePresentModes(vkSurf) - for _, m := range modes { - if m == vk.PresentModeMailbox { - vkPresentMode = m - } - } - - vkRenderPass, err := vkDevice.CreateColorDepthRenderPass(vkFormat, vk.FormatD32Sfloat) - if err != nil { - panic(err.Error()) - } - defer vkDevice.DestroyRenderPass(vkRenderPass) - - // Vulkan Command Pool - vkCommandPool, err := vkDevice.CreateCommandPool(gfx) - if err != nil { - panic(err.Error()) - } - defer vkDevice.DestroyCommandPool(vkCommandPool) - vkCommands, err := vkDevice.AllocateCommandBuffers(vkCommandPool, 1) - if err != nil { - panic(err.Error()) - } - - // MXL Source mxlSrc, err := source.Open(*mxlDomain, *mxlVideoFlowID) if err != nil { - log.Fatalf("source: %v\n", err) + log.Fatalf("source: %v", err) } defer func() { _ = mxlSrc.Close() }() - frameSize := vk.DeviceSize(mxlSrc.Stride()) * vk.DeviceSize(mxlSrc.Height()) - fmt.Printf("source: %dx%d stride=%d frameSize=%d\n", - mxlSrc.Width(), mxlSrc.Height(), mxlSrc.Stride(), frameSize) - // Staging buffer: host-visible, persistently mapped. The reader goroutine - // writes V210 bytes here; the GPU copies from it. - staging, err := vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{ - Size: frameSize, - Usage: vk.BufferUsageTransferSrc, - Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent, - Map: true, + fmt.Printf("source: %dx%d stride=%d\n", mxlSrc.Width(), mxlSrc.Height(), mxlSrc.Stride()) + + r, err := renderer.New(renderer.Config{ + PhysDevice: vkPhysDevice, + Device: vkDevice, + Queue: vkQueue, + Surface: vkSurf, + Window: windowHandler, + GraphicsFamily: gfx, + VideoWidth: mxlSrc.Width(), + VideoHeight: mxlSrc.Height(), + VideoStride: mxlSrc.Stride(), }) if err != nil { panic(err) } - defer vkDevice.DestroyBuffer(staging) - // Device-local V210 buffer: fast for the GPU to read (M3 compute), CPU can't - // write it. Filled each frame by a CopyBuffer from staging. - v210Buf, err := vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{ - Size: frameSize, - Usage: vk.BufferUsageTransferDst | vk.BufferUsageStorageBuffer, - Properties: vk.MemoryDeviceLocal, - Map: false, - }) - if err != nil { - panic(err) - } - defer vkDevice.DestroyBuffer(v210Buf) - - // Decode pipeline: fullscreen triangle, fragment reads V210 from the - // staging buffer and writes 10-bit RGB to the swapchain color attachment. - vertModule, err := vkDevice.CreateShaderModule(vertSPV) - if err != nil { - panic(err) - } - defer vkDevice.DestroyShaderModule(vertModule) - - fragModule, err := vkDevice.CreateShaderModule(fragSPV) - if err != nil { - panic(err) - } - defer vkDevice.DestroyShaderModule(fragModule) - // Descriptor set layout: binding 0 = storage buffer (V210), fragment stage. - decodeDSL, err := vkDevice.CreateDescriptorSetLayout([]vk.DescriptorBinding{ - {Binding: 0, Type: vk.DescriptorStorageBuffer, Count: 1, Stages: vk.ShaderStageFragment}, - }) - if err != nil { - panic(err) - } - defer vkDevice.DestroyDescriptorSetLayout(decodeDSL) - // Pipeline layout: the set layout + push constants {width,height,strideBytes}. - decodeLayout, err := vkDevice.CreatePipelineLayout([]vk.DescriptorSetLayout{decodeDSL}, vk.ShaderStageFragment, 12) - if err != nil { - panic(err) - } - defer vkDevice.DestroyPipelineLayout(decodeLayout) - decodePipeline, err := vkDevice.CreateGraphicsPipeline(vk.GraphicsPipelineConfig{ - Layout: decodeLayout, - RenderPass: vkRenderPass, - VertexShader: vertModule, - FragShader: fragModule, - Topology: vk.TopologyTriangleList, - PolygonMode: vk.PolygonFill, - CullMode: vk.CullNone, - FrontFace: vk.FrontFaceCounterClockwise, - }) - if err != nil { - panic(err) - } - defer vkDevice.DestroyPipeline(decodePipeline) - // Descriptor pool + set, bound once to the staging buffer - descPool, err := vkDevice.CreateDescriptorPool(1, map[vk.DescriptorType]uint32{ - vk.DescriptorStorageBuffer: 1, - }) - if err != nil { - panic(err) - } - defer vkDevice.DestroyDescriptorPool(descPool) - - decodeSet, err := vkDevice.AllocateDescriptorSet(descPool, decodeDSL) - if err != nil { - panic(err) - } - vkDevice.UpdateBufferDescriptor(decodeSet, 0, vk.DescriptorStorageBuffer, staging.Buffer, 0, vk.WholeSize) - - // Swapchain - var ( - vkExtent vk.Extent2D - vkSwapchain vk.SwapchainKHR - swapImages []vk.Image - swapViews []vk.ImageView - depthImg vk.AllocImage - depthView vk.ImageView - fbs []vk.Framebuffer - ) - destroySwapChain := func() { - for _, fb := range fbs { - vkDevice.DestroyFramebuffer(fb) - } - vkDevice.DestroyImageView(depthView) - vkDevice.DestroyImage(depthImg) - for _, v := range swapViews { - vkDevice.DestroyImageView(v) - } - vkDevice.DestroySwapchain(vkSwapchain) - fbs = nil - swapViews = nil - swapImages = nil - depthView = 0 - depthImg = vk.AllocImage{} - vkSwapchain = 0 - vkExtent = vk.Extent2D{} - } - createSwapChain := func() (err error) { - defer func() { - if err != nil { - destroySwapChain() - } - }() - caps, err := vkPhysDevice.SurfaceCapabilities(vkSurf) - if err != nil { - return err - } - //compute swapchain extent - var w, h int32 = WIN_WIDTH, WIN_HEIGHT - if !sdl.GetWindowSizeInPixels(windowHandler, &w, &h) { - return fmt.Errorf("SDL_GetWindowSizeInPixels: %s", sdl.GetError()) - } - if w < int32(caps.MinImageExtent.Width) { - w = int32(caps.MinImageExtent.Width) - } - if h < int32(caps.MinImageExtent.Height) { - h = int32(caps.MinImageExtent.Height) - } - if w > int32(caps.MaxImageExtent.Width) { - w = int32(caps.MaxImageExtent.Width) - } - if h > int32(caps.MaxImageExtent.Height) { - h = int32(caps.MaxImageExtent.Height) - } - if w == 0 || h == 0 { - return errMinimized - } - vkExtent = vk.Extent2D{Width: uint32(w), Height: uint32(h)} - - vkSwapchain, err = vkDevice.CreateSwapchain(vk.SwapchainConfig{ - Surface: vkSurf, - MinImageCount: caps.MinImageCount, - Format: vkFormat, - ColorSpace: vkColorSpace, - Extent: vkExtent, - PresentMode: vkPresentMode, - PreTransform: caps.CurrentTransform, - }) - if err != nil { - return err - } - swapImages, err = vkDevice.SwapchainImages(vkSwapchain) - if err != nil { - return err - } - swapViews = make([]vk.ImageView, len(swapImages)) - for i, img := range swapImages { - v, err := vkDevice.CreateImageView(img, vkFormat, vk.AspectColor) - if err != nil { - return err - } - swapViews[i] = v - } - depthImg, err = vkDevice.CreateImage2D( - vkPhysDevice, vk.FormatD32Sfloat, vkExtent, vk.ImageUsageDepthStencilAttachment) - if err != nil { - return err - } - depthView, err = vkDevice.CreateImageView(depthImg.Image, vk.FormatD32Sfloat, vk.AspectDepth) - if err != nil { - return err - } - fbs = make([]vk.Framebuffer, len(swapViews)) - for i, cv := range swapViews { - fb, err := vkDevice.CreateFramebuffer(vkRenderPass, []vk.ImageView{cv, depthView}, vkExtent) - if err != nil { - return err - } - fbs[i] = fb - } - return nil - } - recreateSwapChain := func() error { - if err := vkDevice.WaitIdle(); err != nil { - return err - } - destroySwapChain() - return createSwapChain() - } - - if err := createSwapChain(); err != nil { - panic(err) - } - defer destroySwapChain() - - // syncs - // Semaphores sync GPU work to GPU work (you never wait on these from the CPU). You need two: - // - imageAvailable — AcquireNextImage signals it when a swapchain image is ready to render into. - // Your draw submission waits on it before touching color. - // - renderFinished — your draw submission signals it when rendering is done. - // Present waits on it before showing the frame. - // Fences sync GPU work to CPU. You need one: - // - inFlight — your submission signals it when the GPU is done with the command buffer. - // Your CPU waits on it before re-recording that same command buffer. - // It's created signaled so the very first frame's wait returns immediately - // (otherwise you'd deadlock waiting for work that hasn't been submitted yet). - imageAvailable, err := vkDevice.CreateSemaphore() - if err != nil { - panic(err.Error()) - } - defer vkDevice.DestroySemaphore(imageAvailable) - - renderFinished, err := vkDevice.CreateSemaphore() - if err != nil { - panic(err.Error()) - } - defer vkDevice.DestroySemaphore(renderFinished) - - inFlight, err := vkDevice.CreateFence(true) - if err != nil { - panic(err.Error()) - } - defer vkDevice.DestroyFence(inFlight) + defer r.Destroy() defer vkDevice.WaitIdle() - // Reader goroutine: stages V210 bytes into the staging buffer. - // Single-flight handshake: it only writes staging after the render thread - // grants permission (post-WaitFence), so the GPU is never reading it ctx, cancel := context.WithCancel(context.Background()) defer cancel() grant := make(chan struct{}, 1) @@ -406,28 +147,10 @@ func main() { s, err := source.Open(*mxlDomain, *mxlVideoFlowID) if err == nil { newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height()) - if newSize != frameSize { - if err := vkDevice.WaitIdle(); err != nil { + if newSize != r.FrameSize() { + if err := r.RecreateBuffers(newSize); err != nil { return err } - vkDevice.DestroyBuffer(v210Buf) - vkDevice.DestroyBuffer(staging) - staging, err = vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{ - Size: newSize, Usage: vk.BufferUsageTransferSrc, - Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent, Map: true, - }) - if err != nil { - return err - } - v210Buf, err = vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{ - Size: newSize, Usage: vk.BufferUsageTransferDst | vk.BufferUsageStorageBuffer, - Properties: vk.MemoryDeviceLocal, Map: false, - }) - if err != nil { - return err - } - frameSize = newSize - vkDevice.UpdateBufferDescriptor(decodeSet, 0, vk.DescriptorStorageBuffer, staging.Buffer, 0, vk.WholeSize) fmt.Printf("source: resolution changed, frameSize=%d\n", newSize) } mxlSrc = s @@ -468,10 +191,7 @@ func main() { cancel() return } - // Copy the borrowed payload into staging BEFORE the next read - // invalidates it. Within the grain's valid lifetime - vk.CopyToMapped(staging.Mapped, f.Payload) - // fmt.Println("staged", f.Index) + vk.CopyToMapped(r.StagingMapped(), f.Payload) select { case staged <- f.Index: case <-ctx.Done(): @@ -480,11 +200,6 @@ func main() { } }() - type decodePushConstants struct { - Width, Height, StrideBytes, WinW, WinH uint32 - } - - // Core Loop running := true resized := false granted := false @@ -521,8 +236,8 @@ func main() { break } if resized { - if err := recreateSwapChain(); err != nil { - if errors.Is(err, errMinimized) { + if err := r.RecreateSwapchain(); err != nil { + if errors.Is(err, renderer.ErrMinimized) { resized = true continue } @@ -545,6 +260,7 @@ func main() { granted = false case <-failed: granted = false + continue case <-ctx.Done(): running = false continue @@ -552,26 +268,18 @@ func main() { continue } - // Acquire the next swapchain image. Right after we have a frame - imageIndex, r := vkDevice.AcquireNextImage(vkSwapchain, imageAvailable, ^uint64(0)) - if r == vk.ErrorOutOfDateKHR || r == vk.SuboptimalKHR { - if err := recreateSwapChain(); err != nil { - if errors.Is(err, errMinimized) { + err := r.DrawFrame(mxlSrc.Width(), mxlSrc.Height(), mxlSrc.Stride()) + if errors.Is(err, renderer.ErrOutOfDate) { + if rerr := r.RecreateSwapchain(); rerr != nil { + if errors.Is(rerr, renderer.ErrMinimized) { resized = true continue } - panic(err) + panic(rerr) } continue } - if !r.Ok() { - panic(fmt.Errorf("acquire: %v", r)) - } - // Wait until GPU is done with our command buffer, then reset fence - if err := vkDevice.WaitFence(inFlight, ^uint64(0)); err != nil { - panic(err) - } - if err := vkDevice.ResetFence(inFlight); err != nil { + if err != nil { panic(err) } @@ -591,71 +299,5 @@ func main() { dropped = 0 lastReport = now } - - // Record: copy staged V210 into device-local buffer, then clear - cmd := vkCommands[0] - if err := cmd.Reset(); err != nil { - panic(err) - } - if err := cmd.Begin(vk.CommandBufferOneTimeSubmit); err != nil { - panic(err) - } - cmd.CopyBuffer(staging.Buffer, v210Buf.Buffer, frameSize) - cmd.BeginRenderPass( - vkRenderPass, - fbs[imageIndex], - vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: vkExtent}, - []vk.ClearValue{ - vk.ClearColor(0.0, 0.4, 0.7, 1.0), - vk.ClearDepthStencil(1.0, 0), - }, - ) - cmd.SetViewport(vk.Viewport{ - X: 0, Y: 0, - Width: float32(vkExtent.Width), - Height: float32(vkExtent.Height), - MinDepth: 0, MaxDepth: 1, - }) - cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: vkExtent}) - cmd.BindPipeline(decodePipeline) - cmd.BindDescriptorSet(decodeLayout, 0, decodeSet) - decodePC := decodePushConstants{ - Width: mxlSrc.Width(), - Height: mxlSrc.Height(), - StrideBytes: mxlSrc.Stride(), - WinW: vkExtent.Width, - WinH: vkExtent.Height, - } - cmd.PushConstants(decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&decodePC), 20) - cmd.Draw(3, 1, 0, 0) - cmd.EndRenderPass() - if err := cmd.End(); err != nil { - panic(err) - } - // Submit: wait on imageAvailable, signal RenderFinished + inFlight - if err := vkQueue.Submit(vk.SubmitConfig{ - Wait: imageAvailable, - WaitStage: vk.StageColorAttachmentOutput, - Command: cmd, - Signal: renderFinished, - Fence: inFlight, - }); err != nil { - panic(err) - } - // Present. Wait on renderFinished - r = vkQueue.Present(vkSwapchain, imageIndex, renderFinished) - if r == vk.ErrorOutOfDateKHR || r == vk.SuboptimalKHR { - if err := recreateSwapChain(); err != nil { - if errors.Is(err, errMinimized) { - resized = true - continue - } - panic(err) - } - continue - } - if !r.Ok() { - panic(fmt.Errorf("present: %v", r)) - } } } diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..1458c58 --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,467 @@ +package renderer + +import ( + "errors" + "fmt" + "mxl-player/internal/sdl" + "unsafe" + + "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") + +// shader push-constants block +type PushConstants struct { + Width, Height, StrideBytes, WinW, WinH uint32 +} + +// all Vulkan state +type Renderer struct { + pd vk.PhysicalDevice + dev vk.Device + queue vk.Queue + surf vk.SurfaceKHR + window uintptr + + format vk.Format + colorSpace uint32 + presentMode vk.PresentMode + renderPass vk.RenderPass + + cmdPool vk.CommandPool + commands []vk.CommandBuffer + + staging vk.AllocBuffer + v210Buf vk.AllocBuffer + frameSize vk.DeviceSize + + vertMod vk.ShaderModule + fragMod vk.ShaderModule + decodeDSL vk.DescriptorSetLayout + decodeLayout vk.PipelineLayout + decodePipeline vk.Pipeline + descPool vk.DescriptorPool + decodeSet vk.DescriptorSet + + extent vk.Extent2D + swapchain vk.SwapchainKHR + swapImages []vk.Image + swapViews []vk.ImageView + depthImg vk.AllocImage + depthView vk.ImageView + fbs []vk.Framebuffer + + imageAvailable vk.Semaphore + renderFinished vk.Semaphore + inFlight vk.Fence +} + +// public params, needed to create a Renderer +type Config struct { + PhysDevice vk.PhysicalDevice + Device vk.Device + Queue vk.Queue + Surface vk.SurfaceKHR + Window uintptr + GraphicsFamily uint32 + VideoWidth, VideoHeight, VideoStride uint32 +} + +// creates Renderer with all Vulkan obkects initialized +func New(cfg Config) (*Renderer, error) { + r := &Renderer{ + pd: cfg.PhysDevice, + dev: cfg.Device, + queue: cfg.Queue, + surf: cfg.Surface, + window: cfg.Window, + } + + // Format: prefer 10-bit A2B10G10R10 (64), fallback to first + // TODO: check other devices, + // maybe it's not the best idea + formats, _ := cfg.PhysDevice.SurfaceFormats(cfg.Surface) + r.format = vk.FormatUndefined + for _, f := range formats { + if f.Format == vk.Format(64) && f.ColorSpace == vk.ColorSpaceSRGBNonlinear { + r.format = f.Format + r.colorSpace = f.ColorSpace + } + } + if r.format == vk.FormatUndefined { + r.format = formats[0].Format + r.colorSpace = formats[0].ColorSpace + } + + // Present mode. Prefer Mailbox, fallback to FIFO + r.presentMode = vk.PresentModeFIFO + modes, _ := cfg.PhysDevice.SurfacePresentModes(cfg.Surface) + for _, m := range modes { + if m == vk.PresentModeMailbox { + r.presentMode = m + } + } + + // Render pass + rp, err := cfg.Device.CreateColorDepthRenderPass(r.format, vk.FormatD32Sfloat) + if err != nil { + return nil, err + } + r.renderPass = rp + + // Command pool + buffers + pool, err := cfg.Device.CreateCommandPool(cfg.GraphicsFamily) + if err != nil { + cfg.Device.DestroyRenderPass(rp) + return nil, err + } + r.cmdPool = pool + cmds, err := cfg.Device.AllocateCommandBuffers(pool, 1) + if err != nil { + cfg.Device.DestroyCommandPool(pool) + cfg.Device.DestroyRenderPass(rp) + return nil, err + } + r.commands = cmds + + // Buffers + r.frameSize = vk.DeviceSize(cfg.VideoStride) * vk.DeviceSize(cfg.VideoHeight) + r.staging, err = cfg.Device.CreateBuffer(cfg.PhysDevice, vk.BufferConfig{ + Size: r.frameSize, + Usage: vk.BufferUsageTransferSrc, + Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent, + Map: true, + }) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + + // Shaders + pipeline + r.vertMod, err = cfg.Device.CreateShaderModule(vertSPV) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + r.fragMod, err = cfg.Device.CreateShaderModule(fragSPV) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + r.decodeDSL, err = cfg.Device.CreateDescriptorSetLayout([]vk.DescriptorBinding{ + {Binding: 0, Type: vk.DescriptorStorageBuffer, Count: 1, Stages: vk.ShaderStageFragment}, + }) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + r.decodeLayout, err = cfg.Device.CreatePipelineLayout( + []vk.DescriptorSetLayout{r.decodeDSL}, vk.ShaderStageFragment, 20) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + r.decodePipeline, err = cfg.Device.CreateGraphicsPipeline(vk.GraphicsPipelineConfig{ + Layout: r.decodeLayout, + RenderPass: r.renderPass, + VertexShader: r.vertMod, + FragShader: r.fragMod, + Topology: vk.TopologyTriangleList, + PolygonMode: vk.PolygonFill, + CullMode: vk.CullNone, + FrontFace: vk.FrontFaceCounterClockwise, + }) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + + // Descriptor pool + set + r.descPool, err = cfg.Device.CreateDescriptorPool(1, map[vk.DescriptorType]uint32{ + vk.DescriptorStorageBuffer: 1, + }) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + r.decodeSet, err = cfg.Device.AllocateDescriptorSet(r.descPool, r.decodeDSL) + if err != nil { + r.destroyPreSwapchain() + return nil, err + } + cfg.Device.UpdateBufferDescriptor(r.decodeSet, 0, vk.DescriptorStorageBuffer, r.staging.Buffer, 0, vk.WholeSize) + + // Swapchain + if err := r.createSwapchain(); err != nil { + r.destroyPreSwapchain() + return nil, err + } + + // Sync objects + r.imageAvailable, err = cfg.Device.CreateSemaphore() + if err != nil { + r.destroyAll() + return nil, err + } + r.renderFinished, err = cfg.Device.CreateSemaphore() + if err != nil { + r.destroyAll() + return nil, err + } + r.inFlight, err = cfg.Device.CreateFence(true) + if err != nil { + r.destroyAll() + return nil, err + } + + return r, nil +} + +func (r *Renderer) createSwapchain() (err error) { + defer func() { + if err != nil { + r.destroySwapchain() + } + }() + caps, err := r.pd.SurfaceCapabilities(r.surf) + if err != nil { + return err + } + var w, h int32 = 1280, 720 + if !sdl.GetWindowSizeInPixels(r.window, &w, &h) { + return fmt.Errorf("SDL_GetWindowSizeInPixels: %s", sdl.GetError()) + } + clamp(&w, int32(caps.MinImageExtent.Width), int32(caps.MaxImageExtent.Width)) + clamp(&h, int32(caps.MinImageExtent.Height), int32(caps.MaxImageExtent.Height)) + if w == 0 || h == 0 { + return ErrMinimized + } + r.extent = vk.Extent2D{Width: uint32(w), Height: uint32(h)} + + r.swapchain, err = r.dev.CreateSwapchain(vk.SwapchainConfig{ + Surface: r.surf, + MinImageCount: caps.MinImageCount, + Format: r.format, + ColorSpace: r.colorSpace, + Extent: r.extent, + PresentMode: r.presentMode, + PreTransform: caps.CurrentTransform, + }) + if err != nil { + return err + } + r.swapImages, err = r.dev.SwapchainImages(r.swapchain) + if err != nil { + return err + } + r.swapViews = make([]vk.ImageView, len(r.swapImages)) + for i, img := range r.swapImages { + v, err := r.dev.CreateImageView(img, r.format, vk.AspectColor) + if err != nil { + return err + } + r.swapViews[i] = v + } + r.depthImg, err = r.dev.CreateImage2D(r.pd, vk.FormatD32Sfloat, r.extent, vk.ImageUsageDepthStencilAttachment) + if err != nil { + return err + } + r.depthView, err = r.dev.CreateImageView(r.depthImg.Image, vk.FormatD32Sfloat, vk.AspectDepth) + if err != nil { + return err + } + r.fbs = make([]vk.Framebuffer, len(r.swapViews)) + for i, cv := range r.swapViews { + fb, err := r.dev.CreateFramebuffer(r.renderPass, []vk.ImageView{cv, r.depthView}, r.extent) + if err != nil { + return err + } + r.fbs[i] = fb + } + + return nil +} + +func (r *Renderer) destroySwapchain() { + for _, fb := range r.fbs { + r.dev.DestroyFramebuffer(fb) + } + r.dev.DestroyImageView(r.depthView) + r.dev.DestroyImage(r.depthImg) + for _, v := range r.swapViews { + r.dev.DestroyImageView(v) + } + r.dev.DestroySwapchain(r.swapchain) + r.fbs = nil + r.swapViews = nil + r.swapImages = nil + r.depthView = 0 + r.depthImg = vk.AllocImage{} + r.swapchain = 0 + r.extent = vk.Extent2D{} +} + +func (r *Renderer) destroyPreSwapchain() { + r.dev.DestroyDescriptorPool(r.descPool) + r.dev.DestroyPipeline(r.decodePipeline) + r.dev.DestroyPipelineLayout(r.decodeLayout) + r.dev.DestroyDescriptorSetLayout(r.decodeDSL) + r.dev.DestroyShaderModule(r.fragMod) + r.dev.DestroyShaderModule(r.vertMod) + r.dev.DestroyBuffer(r.v210Buf) + r.dev.DestroyBuffer(r.staging) + r.dev.DestroyCommandPool(r.cmdPool) + r.dev.DestroyRenderPass(r.renderPass) +} + +func (r *Renderer) destroyAll() { + r.dev.DestroyFence(r.inFlight) + r.dev.DestroySemaphore(r.renderFinished) + r.dev.DestroySemaphore(r.imageAvailable) + r.destroySwapchain() + r.destroyPreSwapchain() +} + +// Tears down everything. Call after Device.WaitIdle +func (r *Renderer) Destroy() { + r.destroyAll() +} + +// Persistently mapped CPU pointer for v210 uploads +func (r *Renderer) StagingMapped() unsafe.Pointer { return r.staging.Mapped } + +// Current swapchain extent +func (r *Renderer) Extent() vk.Extent2D { return r.extent } + +// Current v210 frame size in bytes +func (r *Renderer) FrameSize() vk.DeviceSize { return r.frameSize } + +func clamp(val *int32, min, max int32) { + if *val > max { + *val = max + } else if *val < min { + *val = min + } +} + +// RecreateSwapchain destroys and recreates the swapchain + dependent views/fbs +func (r *Renderer) RecreateSwapchain() error { + if err := r.dev.WaitIdle(); err != nil { + return err + } + r.destroySwapchain() + return r.createSwapchain() +} + +// RecreateBuffers recreates the staging + v210 buffers for a new frame size +func (r *Renderer) RecreateBuffers(newSize vk.DeviceSize) error { + if err := r.dev.WaitIdle(); err != nil { + return err + } + r.dev.DestroyBuffer(r.v210Buf) + r.dev.DestroyBuffer(r.staging) + var err error + r.staging, err = r.dev.CreateBuffer(r.pd, vk.BufferConfig{ + Size: newSize, + Usage: vk.BufferUsageTransferSrc, + Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent, + Map: true, + }) + if err != nil { + return err + } + r.v210Buf, err = r.dev.CreateBuffer(r.pd, vk.BufferConfig{ + Size: newSize, + Usage: vk.BufferUsageTransferDst | vk.BufferUsageStorageBuffer, + Properties: vk.MemoryDeviceLocal, + Map: false, + }) + if err != nil { + return err + } + r.frameSize = newSize + r.dev.UpdateBufferDescriptor(r.decodeSet, 0, vk.DescriptorStorageBuffer, r.staging.Buffer, 0, vk.WholeSize) + return 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 { + imageIndex, res := r.dev.AcquireNextImage(r.swapchain, r.imageAvailable, ^uint64(0)) + if res == vk.ErrorOutOfDateKHR || res == vk.SuboptimalKHR { + return ErrOutOfDate + } + if !res.Ok() { + return fmt.Errorf("acquire: %v", res) + } + + if err := r.dev.WaitFence(r.inFlight, ^uint64(0)); err != nil { + return err + } + if err := r.dev.ResetFence(r.inFlight); err != nil { + return err + } + + cmd := r.commands[0] + if err := cmd.Reset(); err != nil { + return err + } + if err := cmd.Begin(vk.CommandBufferOneTimeSubmit); err != nil { + return err + } + cmd.BeginRenderPass( + r.renderPass, + r.fbs[imageIndex], + vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent}, + []vk.ClearValue{ + vk.ClearColor(0.0, 0.0, 0.0, 1.0), + vk.ClearDepthStencil(1.0, 0), + }, + ) + cmd.SetViewport(vk.Viewport{ + X: 0, Y: 0, + Width: float32(r.extent.Width), + Height: float32(r.extent.Height), + MinDepth: 0, MaxDepth: 1, + }) + cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent}) + cmd.BindPipeline(r.decodePipeline) + cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet) + pc := PushConstants{ + Width: videoW, + Height: videoH, + StrideBytes: stride, + WinW: r.extent.Width, + WinH: r.extent.Height, + } + cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20) + cmd.Draw(3, 1, 0, 0) + cmd.EndRenderPass() + if err := cmd.End(); err != nil { + return err + } + + if err := r.queue.Submit(vk.SubmitConfig{ + Wait: r.imageAvailable, + WaitStage: vk.StageColorAttachmentOutput, + Command: cmd, + Signal: r.renderFinished, + Fence: r.inFlight, + }); err != nil { + return err + } + + res = r.queue.Present(r.swapchain, imageIndex, r.renderFinished) + if res == vk.ErrorOutOfDateKHR || res == vk.SuboptimalKHR { + return ErrOutOfDate + } + if !res.Ok() { + return fmt.Errorf("present: %v", res) + } + return nil +} diff --git a/cmd/mxl-player/shaders.go b/internal/renderer/shaders.go similarity index 88% rename from cmd/mxl-player/shaders.go rename to internal/renderer/shaders.go index 00d5cfa..9362943 100644 --- a/cmd/mxl-player/shaders.go +++ b/internal/renderer/shaders.go @@ -1,4 +1,4 @@ -package main +package renderer import _ "embed" diff --git a/cmd/mxl-player/shaders/decode.frag b/internal/renderer/shaders/decode.frag similarity index 100% rename from cmd/mxl-player/shaders/decode.frag rename to internal/renderer/shaders/decode.frag diff --git a/cmd/mxl-player/shaders/decode.frag.spv b/internal/renderer/shaders/decode.frag.spv similarity index 100% rename from cmd/mxl-player/shaders/decode.frag.spv rename to internal/renderer/shaders/decode.frag.spv diff --git a/cmd/mxl-player/shaders/triangle.vert b/internal/renderer/shaders/triangle.vert similarity index 100% rename from cmd/mxl-player/shaders/triangle.vert rename to internal/renderer/shaders/triangle.vert diff --git a/cmd/mxl-player/shaders/triangle.vert.spv b/internal/renderer/shaders/triangle.vert.spv similarity index 100% rename from cmd/mxl-player/shaders/triangle.vert.spv rename to internal/renderer/shaders/triangle.vert.spv