1//go:build linux
2
3package vkdevice
4
5import (
6 "encoding/binary"
7 "fmt"
8 "image"
9 "unsafe"
10
11 "github.com/bnema/nefergui/internal/presentation/shaders"
12 "github.com/bnema/purego-vulkan/vulkan"
13)
14
15// Rect contains a normalized framebuffer rectangle and straight RGBA color.
16// The fragment stage outputs premultiplied RGBA, including transparent alpha.
17type Rect struct {
18 Bounds [4]float32
19 Color [4]float32
20}
21
22// Instance is a physical-pixel list quad. Color is straight sRGB; the shader
23// premultiplies it. Clip uses x/y/width/height, radii are TL/TR/BR/BL.
24// KindLayer.x = 6 samples set 1 binding 0 as a 2D image texture.
25type Instance struct {
26 Bounds [4]float32
27 Color [4]float32
28 Clip [4]float32
29 Radii [4]float32
30 // UV stores normalized atlas min/max. KindLayer.x is 1 for glyphs;
31 // KindLayer.y selects the array layer. Rectangles keep both zero.
32 UV [4]float32
33 KindLayer [4]float32
34 Widths [4]float32 // top, right, bottom, left
35 Sides [4][4]float32 // top, right, bottom, left straight RGBA
36 Shape [4]float32 // original box for outline/shadow
37 Shadow [4]float32 // offset x/y, blur, spread
38}
39
40// Batch preserves the painter's order. Non-image runs use set 0 (atlas);
41// image draws bind Texture at set 1. Source is borrowed until cache resolution.
42// The image's interface value must be comparable and its pixels immutable.
43// First indexes the shared instance buffer; Count is the instance count.
44type Batch struct {
45 First, Count uint32
46 Source image.Image
47 Texture *ImageTexture
48}
49
50// ImageTexture owns a GPU image and the descriptor slot assigned by the cache.
51// Its lifetime is gated by the last frame fence that sampled it.
52type ImageTexture struct {
53 Set vulkan.DescriptorSet
54}
55
56type Pipeline struct {
57 device *Device
58 Handle vulkan.Pipeline
59 Layout vulkan.PipelineLayout
60}
61
62// NewRectPipeline fixes the Vulkan viewport and scissor to the image size.
63// The pinned Vulkan binding does not expose vkCmdSetViewport/Scissor, so resize
64// builds a new pipeline rather than recording unsupported dynamic-state calls.
65func (d *Device) NewRectPipeline(width, height int32) (p *Pipeline, err error) {
66 return d.newPipeline(width, height, false)
67}
68
69// NewListPipeline creates an instanced, premultiplied-alpha quad pipeline.
70func (d *Device) NewListPipeline(width, height int32) (p *Pipeline, err error) {
71 return d.newPipeline(width, height, true)
72}
73
74func (d *Device) newPipeline(width, height int32, list bool) (p *Pipeline, err error) {
75 if width <= 0 || height <= 0 {
76 return nil, fmt.Errorf("invalid render area %dx%d", width, height)
77 }
78 p = &Pipeline{device: d}
79 defer func() {
80 if err != nil {
81 p.Close()
82 }
83 }()
84 pushSize := uint32(unsafe.Sizeof(Rect{}))
85 if list {
86 pushSize = 8
87 if d.atlas == nil {
88 d.atlas, err = d.newGlyphAtlas()
89 if err != nil {
90 return nil, err
91 }
92 }
93 if d.images == nil {
94 d.images, err = d.newImageCache()
95 if err != nil {
96 return nil, err
97 }
98 }
99 }
100 rangeInfo := vulkan.PushConstantRange{StageFlags: vulkan.ShaderStageVertexBit | vulkan.ShaderStageFragmentBit, Size: pushSize}
101 layoutInfo := vulkan.PipelineLayoutCreateInfo{SType: vulkan.StructureTypePipelineLayoutCreateInfo, PushConstantRangeCount: 1, PushConstantRanges: &rangeInfo}
102 if list {
103 // Set 0 samples the atlas; set 1 samples the image descriptor
104 // bound for the current image batch.
105 layouts := [2]vulkan.DescriptorSetLayout{d.atlas.Layout, d.images.Layout}
106 layoutInfo.SetLayoutCount = uint32(len(layouts))
107 layoutInfo.SetLayouts = &layouts[0]
108 }
109 if err = vulkan.Check(d.Dispatch.CreatePipelineLayout(d.Logical, &layoutInfo, nil, &p.Layout)); err != nil {
110 return nil, fmt.Errorf("create rect pipeline layout: %w", err)
111 }
112 vert, frag := shaders.Vertex, shaders.Fragment
113 if list {
114 vert, frag = shaders.ListVertex, shaders.ListFragment
115 }
116 vertex, err := d.shaderModule(vert)
117 if err != nil {
118 return nil, err
119 }
120 defer d.Dispatch.DestroyShaderModule(d.Logical, vertex, nil)
121 fragment, err := d.shaderModule(frag)
122 if err != nil {
123 return nil, err
124 }
125 defer d.Dispatch.DestroyShaderModule(d.Logical, fragment, nil)
126 main := []byte("main\x00")
127 stages := [2]vulkan.PipelineShaderStageCreateInfo{
128 {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageVertexBit, Module: vertex, Name: &main[0]},
129 {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageFragmentBit, Module: fragment, Name: &main[0]},
130 }
131 vertexInput := vulkan.PipelineVertexInputStateCreateInfo{SType: vulkan.StructureTypePipelineVertexInputStateCreateInfo}
132 var binding vulkan.VertexInputBindingDescription
133 var attrs [13]vulkan.VertexInputAttributeDescription
134 if list {
135 binding = vulkan.VertexInputBindingDescription{Stride: uint32(unsafe.Sizeof(Instance{})), InputRate: vulkan.VertexInputRateInstance}
136 offsets := [13]uint32{uint32(unsafe.Offsetof(Instance{}.Bounds)), uint32(unsafe.Offsetof(Instance{}.Color)), uint32(unsafe.Offsetof(Instance{}.Clip)), uint32(unsafe.Offsetof(Instance{}.Radii)), uint32(unsafe.Offsetof(Instance{}.UV)), uint32(unsafe.Offsetof(Instance{}.KindLayer)), uint32(unsafe.Offsetof(Instance{}.Widths)), uint32(unsafe.Offsetof(Instance{}.Sides)), uint32(unsafe.Offsetof(Instance{}.Sides)) + 16, uint32(unsafe.Offsetof(Instance{}.Sides)) + 32, uint32(unsafe.Offsetof(Instance{}.Sides)) + 48, uint32(unsafe.Offsetof(Instance{}.Shape)), uint32(unsafe.Offsetof(Instance{}.Shadow))}
137 for i := range attrs {
138 attrs[i] = vulkan.VertexInputAttributeDescription{Location: uint32(i), Format: vulkan.FormatR32g32b32a32Sfloat, Offset: offsets[i]}
139 }
140 vertexInput.VertexBindingDescriptionCount = 1
141 vertexInput.VertexBindingDescriptions = &binding
142 vertexInput.VertexAttributeDescriptionCount = uint32(len(attrs))
143 vertexInput.VertexAttributeDescriptions = &attrs[0]
144 }
145 assembly := vulkan.PipelineInputAssemblyStateCreateInfo{SType: vulkan.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vulkan.PrimitiveTopologyTriangleList}
146 viewport := vulkan.Viewport{Width: float32(width), Height: float32(height), MaxDepth: 1}
147 scissor := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(width), Height: uint32(height)}}
148 vp := vulkan.PipelineViewportStateCreateInfo{SType: vulkan.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
149 raster := vulkan.PipelineRasterizationStateCreateInfo{SType: vulkan.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vulkan.PolygonModeFill, CullMode: vulkan.CullModeNone, FrontFace: vulkan.FrontFaceCounterClockwise, LineWidth: 1}
150 ms := vulkan.PipelineMultisampleStateCreateInfo{SType: vulkan.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vulkan.SampleCount1Bit}
151 blendAttachment := vulkan.PipelineColorBlendAttachmentState{BlendEnable: 1, SrcColorBlendFactor: vulkan.BlendFactorOne, DstColorBlendFactor: vulkan.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vulkan.BlendOpAdd, SrcAlphaBlendFactor: vulkan.BlendFactorOne, DstAlphaBlendFactor: vulkan.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vulkan.BlendOpAdd, ColorWriteMask: vulkan.ColorComponentRBit | vulkan.ColorComponentGBit | vulkan.ColorComponentBBit | vulkan.ColorComponentABit}
152 blend := vulkan.PipelineColorBlendStateCreateInfo{SType: vulkan.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &blendAttachment}
153 format := vulkan.Format(vulkan.FormatB8g8r8a8Unorm)
154 rendering := vulkan.PipelineRenderingCreateInfo{SType: vulkan.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
155 ci := vulkan.GraphicsPipelineCreateInfo{SType: vulkan.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering), StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly, ViewportState: &vp, RasterizationState: &raster, MultisampleState: &ms, ColorBlendState: &blend, Layout: p.Layout}
156 if err = vulkan.Check(d.Dispatch.CreateGraphicsPipelines(d.Logical, 0, 1, &ci, nil, &p.Handle)); err != nil {
157 return nil, fmt.Errorf("create rectangle pipeline with dynamic rendering: %w", err)
158 }
159 return p, nil
160}
161
162func (d *Device) shaderModule(data []byte) (vulkan.ShaderModule, error) {
163 if len(data) < 20 || len(data)%4 != 0 || binary.LittleEndian.Uint32(data) != 0x07230203 {
164 return 0, fmt.Errorf("invalid embedded SPIR-V")
165 }
166 words := make([]uint32, len(data)/4)
167 for i := range words {
168 words[i] = binary.LittleEndian.Uint32(data[i*4:])
169 }
170 ci := vulkan.ShaderModuleCreateInfo{SType: vulkan.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(data)), Code: &words[0]}
171 var module vulkan.ShaderModule
172 if err := vulkan.Check(d.Dispatch.CreateShaderModule(d.Logical, &ci, nil, &module)); err != nil {
173 return 0, err
174 }
175 return module, nil
176}
177
178func (p *Pipeline) Close() {
179 if p == nil || p.device == nil {
180 return
181 }
182 if p.Handle != 0 {
183 p.device.Dispatch.DestroyPipeline(p.device.Logical, p.Handle, nil)
184 p.Handle = 0
185 }
186 if p.Layout != 0 {
187 p.device.Dispatch.DestroyPipelineLayout(p.device.Logical, p.Layout, nil)
188 p.Layout = 0
189 }
190}