// SPDX-License-Identifier: Unlicense OR MIT

//go:build (linux || freebsd) && !novulkan
// +build linux freebsd
// +build !novulkan

package vulkan

import (
	"errors"
	"fmt"
	"image"
	"math/bits"

	"gioui.org/gpu/internal/driver"
	"gioui.org/internal/vk"
	"gioui.org/shader"
)

type Backend struct {
	physDev vk.PhysicalDevice
	dev     vk.Device
	queue   vk.Queue
	cmdPool struct {
		current vk.CommandBuffer
		pool    vk.CommandPool
		used    int
		buffers []vk.CommandBuffer
	}
	outFormat vk.Format
	staging   struct {
		buf  *Buffer
		mem  []byte
		size int
		cap  int
	}
	defers     []func(d vk.Device)
	frameSig   vk.Semaphore
	frameFence vk.Fence
	waitSems   []vk.Semaphore
	waitStages []vk.PipelineStageFlags
	sigSems    []vk.Semaphore
	fence      vk.Fence

	allPipes []*Pipeline

	pipe *Pipeline

	passes map[passKey]vk.RenderPass

	// bindings and offset are temporary storage for BindVertexBuffer.
	bindings []vk.Buffer
	offsets  []vk.DeviceSize

	desc struct {
		dirty    bool
		texBinds [texUnits]*Texture
		bufBinds [storageUnits]*Buffer
	}

	caps driver.Features
}

type passKey struct {
	fmt         vk.Format
	loadAct     vk.AttachmentLoadOp
	initLayout  vk.ImageLayout
	finalLayout vk.ImageLayout
}

type Texture struct {
	backend    *Backend
	img        vk.Image
	mem        vk.DeviceMemory
	view       vk.ImageView
	sampler    vk.Sampler
	fbo        vk.Framebuffer
	format     vk.Format
	mipmaps    int
	layout     vk.ImageLayout
	passLayout vk.ImageLayout
	width      int
	height     int
	acquire    vk.Semaphore
	foreign    bool

	scope struct {
		stage  vk.PipelineStageFlags
		access vk.AccessFlags
	}
}

type Shader struct {
	dev       vk.Device
	module    vk.ShaderModule
	pushRange vk.PushConstantRange
	src       shader.Sources
}

type Pipeline struct {
	backend    *Backend
	pipe       vk.Pipeline
	pushRanges []vk.PushConstantRange
	ninputs    int
	desc       *descPool
}

type descPool struct {
	layout     vk.PipelineLayout
	descLayout vk.DescriptorSetLayout
	pool       vk.DescriptorPool
	sets       []vk.DescriptorSet
	size       int
	texBinds   []int
	imgBinds   []int
	bufBinds   []int
}

type Buffer struct {
	backend *Backend
	buf     vk.Buffer
	store   []byte
	mem     vk.DeviceMemory
	usage   vk.BufferUsageFlags

	scope struct {
		stage  vk.PipelineStageFlags
		access vk.AccessFlags
	}
}

const (
	texUnits     = 4
	storageUnits = 4
)

func init() {
	driver.NewVulkanDevice = newVulkanDevice
}

func newVulkanDevice(api driver.Vulkan) (driver.Device, error) {
	b := &Backend{
		physDev:   vk.PhysicalDevice(api.PhysDevice),
		dev:       vk.Device(api.Device),
		outFormat: vk.Format(api.Format),
		caps:      driver.FeatureCompute,
		passes:    make(map[passKey]vk.RenderPass),
	}
	b.queue = vk.GetDeviceQueue(b.dev, api.QueueFamily, api.QueueIndex)
	cmdPool, err := vk.CreateCommandPool(b.dev, api.QueueFamily)
	if err != nil {
		return nil, err
	}
	b.cmdPool.pool = cmdPool
	props := vk.GetPhysicalDeviceFormatProperties(b.physDev, vk.FORMAT_R16_SFLOAT)
	reqs := vk.FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_BIT
	if props&reqs == reqs {
		b.caps |= driver.FeatureFloatRenderTargets
	}
	reqs = vk.FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT
	props = vk.GetPhysicalDeviceFormatProperties(b.physDev, vk.FORMAT_R8G8B8A8_SRGB)
	if props&reqs == reqs {
		b.caps |= driver.FeatureSRGB
	}
	fence, err := vk.CreateFence(b.dev, 0)
	if err != nil {
		return nil, mapErr(err)
	}
	b.fence = fence
	return b, nil
}

func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture {
	b.staging.size = 0
	b.cmdPool.used = 0
	b.runDefers()
	b.resetPipes()

	if target == nil {
		return nil
	}
	switch t := target.(type) {
	case driver.VulkanRenderTarget:
		layout := vk.IMAGE_LAYOUT_UNDEFINED
		if !clear {
			layout = vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
		}
		b.frameSig = vk.Semaphore(t.SignalSem)
		b.frameFence = vk.Fence(t.Fence)
		tex := &Texture{
			img:        vk.Image(t.Image),
			fbo:        vk.Framebuffer(t.Framebuffer),
			width:      viewport.X,
			height:     viewport.Y,
			layout:     layout,
			passLayout: vk.IMAGE_LAYOUT_PRESENT_SRC_KHR,
			format:     b.outFormat,
			acquire:    vk.Semaphore(t.WaitSem),
			foreign:    true,
		}
		return tex
	case *Texture:
		return t
	default:
		panic(fmt.Sprintf("vulkan: unsupported render target type: %T", t))
	}
}

func (b *Backend) deferFunc(f func(d vk.Device)) {
	b.defers = append(b.defers, f)
}

func (b *Backend) runDefers() {
	for _, f := range b.defers {
		f(b.dev)
	}
	b.defers = b.defers[:0]
}

func (b *Backend) resetPipes() {
	for i := len(b.allPipes) - 1; i >= 0; i-- {
		p := b.allPipes[i]
		if p.pipe == 0 {
			// Released pipeline.
			b.allPipes = append(b.allPipes[:i], b.allPipes[:i+1]...)
			continue
		}
		if p.desc.size > 0 {
			p.desc.size = 0
		}
	}
}

func (b *Backend) EndFrame() {
	if b.frameSig != 0 {
		b.sigSems = append(b.sigSems, b.frameSig)
		b.frameSig = 0
	}
	fence := b.frameFence
	if fence == 0 {
		// We're internally synchronized.
		fence = b.fence
	}
	b.submitCmdBuf(fence)
	if b.frameFence == 0 {
		vk.WaitForFences(b.dev, fence)
		vk.ResetFences(b.dev, fence)
	}
}

func (b *Backend) Caps() driver.Caps {
	return driver.Caps{
		MaxTextureSize: 4096,
		Features:       b.caps,
	}
}

func (b *Backend) NewTimer() driver.Timer {
	panic("timers not supported")
}

func (b *Backend) IsTimeContinuous() bool {
	panic("timers not supported")
}

func (b *Backend) Release() {
	vk.DeviceWaitIdle(b.dev)
	if buf := b.staging.buf; buf != nil {
		vk.UnmapMemory(b.dev, b.staging.buf.mem)
		buf.Release()
	}
	b.runDefers()
	for _, rp := range b.passes {
		vk.DestroyRenderPass(b.dev, rp)
	}
	vk.DestroyFence(b.dev, b.fence)
	vk.FreeCommandBuffers(b.dev, b.cmdPool.pool, b.cmdPool.buffers...)
	vk.DestroyCommandPool(b.dev, b.cmdPool.pool)
	*b = Backend{}
}

func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) {
	vkfmt := formatFor(format)
	usage := vk.IMAGE_USAGE_TRANSFER_DST_BIT | vk.IMAGE_USAGE_TRANSFER_SRC_BIT
	passLayout := vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
	if bindings&driver.BufferBindingTexture != 0 {
		usage |= vk.IMAGE_USAGE_SAMPLED_BIT
		passLayout = vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
	}
	if bindings&driver.BufferBindingFramebuffer != 0 {
		usage |= vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT
	}
	if bindings&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0 {
		usage |= vk.IMAGE_USAGE_STORAGE_BIT
	}
	filterFor := func(f driver.TextureFilter) vk.Filter {
		switch f {
		case driver.FilterLinear, driver.FilterLinearMipmapLinear:
			return vk.FILTER_LINEAR
		case driver.FilterNearest:
			return vk.FILTER_NEAREST
		}
		panic("unknown filter")
	}
	mipmapMode := vk.SAMPLER_MIPMAP_MODE_NEAREST
	mipmap := minFilter == driver.FilterLinearMipmapLinear
	nmipmaps := 1
	if mipmap {
		mipmapMode = vk.SAMPLER_MIPMAP_MODE_LINEAR
		dim := width
		if height > dim {
			dim = height
		}
		log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1
		nmipmaps = log2 + 1
	}
	sampler, err := vk.CreateSampler(b.dev, filterFor(minFilter), filterFor(magFilter), mipmapMode)
	if err != nil {
		return nil, mapErr(err)
	}
	img, mem, err := vk.CreateImage(b.physDev, b.dev, vkfmt, width, height, nmipmaps, usage)
	if err != nil {
		vk.DestroySampler(b.dev, sampler)
		return nil, mapErr(err)
	}
	view, err := vk.CreateImageView(b.dev, img, vkfmt)
	if err != nil {
		vk.DestroySampler(b.dev, sampler)
		vk.DestroyImage(b.dev, img)
		vk.FreeMemory(b.dev, mem)
		return nil, mapErr(err)
	}
	t := &Texture{backend: b, img: img, mem: mem, view: view, sampler: sampler, layout: vk.IMAGE_LAYOUT_UNDEFINED, passLayout: passLayout, width: width, height: height, format: vkfmt, mipmaps: nmipmaps}
	if bindings&driver.BufferBindingFramebuffer != 0 {
		pass, err := vk.CreateRenderPass(b.dev, vkfmt, vk.ATTACHMENT_LOAD_OP_DONT_CARE,
			vk.IMAGE_LAYOUT_UNDEFINED, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, nil)
		if err != nil {
			return nil, mapErr(err)
		}
		defer vk.DestroyRenderPass(b.dev, pass)
		fbo, err := vk.CreateFramebuffer(b.dev, pass, view, width, height)
		if err != nil {
			return nil, mapErr(err)
		}
		t.fbo = fbo
	}
	return t, nil
}

func (b *Backend) NewBuffer(bindings driver.BufferBinding, size int) (driver.Buffer, error) {
	if bindings&driver.BufferBindingUniforms != 0 {
		// Implement uniform buffers as inline push constants.
		return &Buffer{store: make([]byte, size)}, nil
	}
	usage := vk.BUFFER_USAGE_TRANSFER_DST_BIT | vk.BUFFER_USAGE_TRANSFER_SRC_BIT
	if bindings&driver.BufferBindingIndices != 0 {
		usage |= vk.BUFFER_USAGE_INDEX_BUFFER_BIT
	}
	if bindings&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0 {
		usage |= vk.BUFFER_USAGE_STORAGE_BUFFER_BIT
	}
	if bindings&driver.BufferBindingVertices != 0 {
		usage |= vk.BUFFER_USAGE_VERTEX_BUFFER_BIT
	}
	buf, err := b.newBuffer(size, usage, vk.MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
	return buf, mapErr(err)
}

func (b *Backend) newBuffer(size int, usage vk.BufferUsageFlags, props vk.MemoryPropertyFlags) (*Buffer, error) {
	buf, mem, err := vk.CreateBuffer(b.physDev, b.dev, size, usage, props)
	return &Buffer{backend: b, buf: buf, mem: mem, usage: usage}, err
}

func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) {
	buf, err := b.NewBuffer(typ, len(data))
	if err != nil {
		return nil, err
	}
	buf.Upload(data)
	return buf, nil
}

func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) {
	sh, err := b.newShader(src, vk.SHADER_STAGE_VERTEX_BIT)
	return sh, mapErr(err)
}

func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) {
	sh, err := b.newShader(src, vk.SHADER_STAGE_FRAGMENT_BIT)
	return sh, mapErr(err)
}

func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) {
	vs := desc.VertexShader.(*Shader)
	fs := desc.FragmentShader.(*Shader)
	var ranges []vk.PushConstantRange
	if r := vs.pushRange; r != (vk.PushConstantRange{}) {
		ranges = append(ranges, r)
	}
	if r := fs.pushRange; r != (vk.PushConstantRange{}) {
		ranges = append(ranges, r)
	}
	descPool, err := createPipelineLayout(b.dev, fs.src, ranges)
	if err != nil {
		return nil, mapErr(err)
	}
	blend := desc.BlendDesc
	factorFor := func(f driver.BlendFactor) vk.BlendFactor {
		switch f {
		case driver.BlendFactorZero:
			return vk.BLEND_FACTOR_ZERO
		case driver.BlendFactorOne:
			return vk.BLEND_FACTOR_ONE
		case driver.BlendFactorOneMinusSrcAlpha:
			return vk.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA
		case driver.BlendFactorDstColor:
			return vk.BLEND_FACTOR_DST_COLOR
		default:
			panic("unknown blend factor")
		}
	}
	var top vk.PrimitiveTopology
	switch desc.Topology {
	case driver.TopologyTriangles:
		top = vk.PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
	case driver.TopologyTriangleStrip:
		top = vk.PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
	default:
		panic("unknown topology")
	}
	var binds []vk.VertexInputBindingDescription
	var attrs []vk.VertexInputAttributeDescription
	inputs := desc.VertexLayout.Inputs
	for i, inp := range inputs {
		binds = append(binds, vk.VertexInputBindingDescription{
			Binding: i,
			Stride:  desc.VertexLayout.Stride,
		})
		attrs = append(attrs, vk.VertexInputAttributeDescription{
			Binding:  i,
			Location: vs.src.Inputs[i].Location,
			Format:   vertFormatFor(vs.src.Inputs[i]),
			Offset:   inp.Offset,
		})
	}
	fmt := b.outFormat
	if f := desc.PixelFormat; f != driver.TextureFormatOutput {
		fmt = formatFor(f)
	}
	pass, err := vk.CreateRenderPass(b.dev, fmt, vk.ATTACHMENT_LOAD_OP_DONT_CARE,
		vk.IMAGE_LAYOUT_UNDEFINED, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, nil)
	if err != nil {
		return nil, mapErr(err)
	}
	defer vk.DestroyRenderPass(b.dev, pass)
	pipe, err := vk.CreateGraphicsPipeline(b.dev, pass, vs.module, fs.module, blend.Enable, factorFor(blend.SrcFactor), factorFor(blend.DstFactor), top, binds, attrs, descPool.layout)
	if err != nil {
		descPool.release(b.dev)
		return nil, mapErr(err)
	}
	p := &Pipeline{backend: b, pipe: pipe, desc: descPool, pushRanges: ranges, ninputs: len(inputs)}
	b.allPipes = append(b.allPipes, p)
	return p, nil
}

func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) {
	sh, err := b.newShader(src, vk.SHADER_STAGE_COMPUTE_BIT)
	if err != nil {
		return nil, mapErr(err)
	}
	defer sh.Release()
	descPool, err := createPipelineLayout(b.dev, src, nil)
	if err != nil {
		return nil, mapErr(err)
	}
	pipe, err := vk.CreateComputePipeline(b.dev, sh.module, descPool.layout)
	if err != nil {
		descPool.release(b.dev)
		return nil, mapErr(err)
	}
	return &Pipeline{backend: b, pipe: pipe, desc: descPool}, nil
}

func vertFormatFor(f shader.InputLocation) vk.Format {
	t := f.Type
	s := f.Size
	switch {
	case t == shader.DataTypeFloat && s == 1:
		return vk.FORMAT_R32_SFLOAT
	case t == shader.DataTypeFloat && s == 2:
		return vk.FORMAT_R32G32_SFLOAT
	case t == shader.DataTypeFloat && s == 3:
		return vk.FORMAT_R32G32B32_SFLOAT
	case t == shader.DataTypeFloat && s == 4:
		return vk.FORMAT_R32G32B32A32_SFLOAT
	default:
		panic("unsupported data type")
	}
}

func createPipelineLayout(d vk.Device, src shader.Sources, ranges []vk.PushConstantRange) (*descPool, error) {
	var (
		descLayouts []vk.DescriptorSetLayout
		descLayout  vk.DescriptorSetLayout
	)
	texBinds := make([]int, len(src.Textures))
	imgBinds := make([]int, len(src.Images))
	bufBinds := make([]int, len(src.StorageBuffers))
	var descBinds []vk.DescriptorSetLayoutBinding
	for i, t := range src.Textures {
		descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{
			Binding:        t.Binding,
			StageFlags:     vk.SHADER_STAGE_FRAGMENT_BIT,
			DescriptorType: vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
		})
		texBinds[i] = t.Binding
	}
	for i, img := range src.Images {
		descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{
			Binding:        img.Binding,
			StageFlags:     vk.SHADER_STAGE_COMPUTE_BIT,
			DescriptorType: vk.DESCRIPTOR_TYPE_STORAGE_IMAGE,
		})
		imgBinds[i] = img.Binding
	}
	for i, buf := range src.StorageBuffers {
		descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{
			Binding:        buf.Binding,
			StageFlags:     vk.SHADER_STAGE_COMPUTE_BIT,
			DescriptorType: vk.DESCRIPTOR_TYPE_STORAGE_BUFFER,
		})
		bufBinds[i] = buf.Binding
	}
	if len(descBinds) > 0 {
		var err error
		descLayout, err = vk.CreateDescriptorSetLayout(d, descBinds)
		if err != nil {
			return nil, err
		}
		descLayouts = append(descLayouts, descLayout)
	}
	layout, err := vk.CreatePipelineLayout(d, ranges, descLayouts)
	if err != nil {
		if descLayout != 0 {
			vk.DestroyDescriptorSetLayout(d, descLayout)
		}
		return nil, err
	}
	descPool := &descPool{
		texBinds:   texBinds,
		bufBinds:   bufBinds,
		imgBinds:   imgBinds,
		layout:     layout,
		descLayout: descLayout,
	}
	return descPool, nil
}

func (b *Backend) newShader(src shader.Sources, stage vk.ShaderStageFlags) (*Shader, error) {
	mod, err := vk.CreateShaderModule(b.dev, src.SPIRV)
	if err != nil {
		return nil, err
	}

	sh := &Shader{dev: b.dev, module: mod, src: src}
	if locs := src.Uniforms.Locations; len(locs) > 0 {
		pushOffset := 0x7fffffff
		for _, l := range locs {
			if l.Offset < pushOffset {
				pushOffset = l.Offset
			}
		}
		sh.pushRange = vk.BuildPushConstantRange(stage, pushOffset, src.Uniforms.Size)
	}
	return sh, nil
}

func (b *Backend) CopyTexture(dstTex driver.Texture, dorig image.Point, srcFBO driver.Texture, srect image.Rectangle) {
	dst := dstTex.(*Texture)
	src := srcFBO.(*Texture)
	cmdBuf := b.ensureCmdBuf()
	op := vk.BuildImageCopy(srect.Min.X, srect.Min.Y, dorig.X, dorig.Y, srect.Dx(), srect.Dy())
	src.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_READ_BIT,
	)
	dst.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_WRITE_BIT,
	)
	vk.CmdCopyImage(cmdBuf, src.img, src.layout, dst.img, dst.layout, []vk.ImageCopy{op})
}

func (b *Backend) Viewport(x, y, width, height int) {
	cmdBuf := b.currentCmdBuf()
	vp := vk.BuildViewport(float32(x), float32(y), float32(width), float32(height))
	vk.CmdSetViewport(cmdBuf, 0, vp)
}

func (b *Backend) DrawArrays(off, count int) {
	cmdBuf := b.currentCmdBuf()
	if b.desc.dirty {
		b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_GRAPHICS, b.desc.texBinds, b.desc.bufBinds)
		b.desc.dirty = false
	}
	vk.CmdDraw(cmdBuf, count, 1, off, 0)
}

func (b *Backend) DrawElements(off, count int) {
	cmdBuf := b.currentCmdBuf()
	if b.desc.dirty {
		b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_GRAPHICS, b.desc.texBinds, b.desc.bufBinds)
		b.desc.dirty = false
	}
	vk.CmdDrawIndexed(cmdBuf, count, 1, off, 0, 0)
}

func (b *Backend) BindImageTexture(unit int, tex driver.Texture) {
	t := tex.(*Texture)
	b.desc.texBinds[unit] = t
	b.desc.dirty = true
	t.imageBarrier(b.currentCmdBuf(),
		vk.IMAGE_LAYOUT_GENERAL,
		vk.PIPELINE_STAGE_COMPUTE_SHADER_BIT,
		vk.ACCESS_SHADER_READ_BIT|vk.ACCESS_SHADER_WRITE_BIT,
	)
}

func (b *Backend) DispatchCompute(x, y, z int) {
	cmdBuf := b.currentCmdBuf()
	if b.desc.dirty {
		b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_COMPUTE, b.desc.texBinds, b.desc.bufBinds)
		b.desc.dirty = false
	}
	vk.CmdDispatch(cmdBuf, x, y, z)
}

func (t *Texture) Upload(offset, size image.Point, pixels []byte, stride int) {
	if stride == 0 {
		stride = size.X * 4
	}
	cmdBuf := t.backend.ensureCmdBuf()
	dstStride := size.X * 4
	n := size.Y * dstStride
	stage, mem, off := t.backend.stagingBuffer(n)
	var srcOff, dstOff int
	for y := 0; y < size.Y; y++ {
		srcRow := pixels[srcOff : srcOff+dstStride]
		dstRow := mem[dstOff : dstOff+dstStride]
		copy(dstRow, srcRow)
		dstOff += dstStride
		srcOff += stride
	}
	op := vk.BuildBufferImageCopy(off, dstStride/4, offset.X, offset.Y, size.X, size.Y)
	t.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_WRITE_BIT,
	)
	vk.CmdCopyBufferToImage(cmdBuf, stage.buf, t.img, t.layout, op)
	// Build mipmaps by repeating linear blits.
	w, h := t.width, t.height
	for i := 1; i < t.mipmaps; i++ {
		nw, nh := w/2, h/2
		if nh < 1 {
			nh = 1
		}
		if nw < 1 {
			nw = 1
		}
		// Transition previous (source) level.
		b := vk.BuildImageMemoryBarrier(
			t.img,
			vk.ACCESS_TRANSFER_WRITE_BIT, vk.ACCESS_TRANSFER_READ_BIT,
			vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			i-1, 1,
		)
		vk.CmdPipelineBarrier(cmdBuf, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b})
		// Blit to this mipmap level.
		blit := vk.BuildImageBlit(0, 0, 0, 0, w, h, nw, nh, i-1, i)
		vk.CmdBlitImage(cmdBuf, t.img, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, t.img, vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, []vk.ImageBlit{blit}, vk.FILTER_LINEAR)
		w, h = nw, nh
	}
	if t.mipmaps > 1 {
		// Add barrier for last blit.
		b := vk.BuildImageMemoryBarrier(
			t.img,
			vk.ACCESS_TRANSFER_WRITE_BIT, vk.ACCESS_TRANSFER_READ_BIT,
			vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			t.mipmaps-1, 1,
		)
		vk.CmdPipelineBarrier(cmdBuf, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b})
		t.layout = vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
	}
}

func (t *Texture) Release() {
	if t.foreign {
		panic("external textures cannot be released")
	}
	freet := *t
	t.backend.deferFunc(func(d vk.Device) {
		if freet.fbo != 0 {
			vk.DestroyFramebuffer(d, freet.fbo)
		}
		vk.DestroySampler(d, freet.sampler)
		vk.DestroyImageView(d, freet.view)
		vk.DestroyImage(d, freet.img)
		vk.FreeMemory(d, freet.mem)
	})
	*t = Texture{}
}

func (p *Pipeline) Release() {
	freep := *p
	p.backend.deferFunc(func(d vk.Device) {
		freep.desc.release(d)
		vk.DestroyPipeline(d, freep.pipe)
	})
	*p = Pipeline{}
}

func (p *descPool) release(d vk.Device) {
	if p := p.pool; p != 0 {
		vk.DestroyDescriptorPool(d, p)
	}
	if l := p.descLayout; l != 0 {
		vk.DestroyDescriptorSetLayout(d, l)
	}
	vk.DestroyPipelineLayout(d, p.layout)
}

func (p *descPool) bindDescriptorSet(b *Backend, cmdBuf vk.CommandBuffer, bindPoint vk.PipelineBindPoint, texBinds [texUnits]*Texture, bufBinds [storageUnits]*Buffer) {
	if p.size == len(p.sets) {
		l := p.descLayout
		if l == 0 {
			panic("vulkan: descriptor set is dirty, but pipeline has empty layout")
		}
		newCap := len(p.sets) * 2
		if pool := p.pool; pool != 0 {
			b.deferFunc(func(d vk.Device) {
				vk.DestroyDescriptorPool(d, pool)
			})
		}
		const initialPoolSize = 100
		if newCap < initialPoolSize {
			newCap = initialPoolSize
		}
		var poolSizes []vk.DescriptorPoolSize
		if n := len(p.texBinds); n > 0 {
			poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, newCap*n))
		}
		if n := len(p.imgBinds); n > 0 {
			poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_STORAGE_IMAGE, newCap*n))
		}
		if n := len(p.bufBinds); n > 0 {
			poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_STORAGE_BUFFER, newCap*n))
		}
		pool, err := vk.CreateDescriptorPool(b.dev, newCap, poolSizes)
		if err != nil {
			panic(fmt.Errorf("vulkan: failed to allocate descriptor pool with %d descriptors: %v", newCap, err))
		}
		p.pool = pool
		sets, err := vk.AllocateDescriptorSets(b.dev, p.pool, l, newCap)
		if err != nil {
			panic(fmt.Errorf("vulkan: failed to allocate descriptor with %d sets: %v", newCap, err))
		}
		p.sets = sets
		p.size = 0
	}
	descSet := p.sets[p.size]
	p.size++
	for _, bind := range p.texBinds {
		tex := texBinds[bind]
		write := vk.BuildWriteDescriptorSetImage(descSet, bind, vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, tex.sampler, tex.view, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
		vk.UpdateDescriptorSet(b.dev, write)
	}
	for _, bind := range p.imgBinds {
		tex := texBinds[bind]
		write := vk.BuildWriteDescriptorSetImage(descSet, bind, vk.DESCRIPTOR_TYPE_STORAGE_IMAGE, 0, tex.view, vk.IMAGE_LAYOUT_GENERAL)
		vk.UpdateDescriptorSet(b.dev, write)
	}
	for _, bind := range p.bufBinds {
		buf := bufBinds[bind]
		write := vk.BuildWriteDescriptorSetBuffer(descSet, bind, vk.DESCRIPTOR_TYPE_STORAGE_BUFFER, buf.buf)
		vk.UpdateDescriptorSet(b.dev, write)
	}
	vk.CmdBindDescriptorSets(cmdBuf, bindPoint, p.layout, 0, []vk.DescriptorSet{descSet})
}

func (t *Texture) imageBarrier(cmdBuf vk.CommandBuffer, layout vk.ImageLayout, stage vk.PipelineStageFlags, access vk.AccessFlags) {
	srcStage := t.scope.stage
	if srcStage == 0 && t.layout == layout {
		t.scope.stage = stage
		t.scope.access = access
		return
	}
	if srcStage == 0 {
		srcStage = vk.PIPELINE_STAGE_TOP_OF_PIPE_BIT
	}
	b := vk.BuildImageMemoryBarrier(
		t.img,
		t.scope.access, access,
		t.layout, layout,
		0, vk.REMAINING_MIP_LEVELS,
	)
	vk.CmdPipelineBarrier(cmdBuf, srcStage, stage, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b})
	t.layout = layout
	t.scope.stage = stage
	t.scope.access = access
}

func (b *Backend) PrepareTexture(tex driver.Texture) {
	t := tex.(*Texture)
	cmdBuf := b.ensureCmdBuf()
	t.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
		vk.PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
		vk.ACCESS_SHADER_READ_BIT,
	)
}

func (b *Backend) BindTexture(unit int, tex driver.Texture) {
	t := tex.(*Texture)
	b.desc.texBinds[unit] = t
	b.desc.dirty = true
}

func (b *Backend) BindPipeline(pipe driver.Pipeline) {
	b.bindPipeline(pipe.(*Pipeline), vk.PIPELINE_BIND_POINT_GRAPHICS)
}

func (b *Backend) BindProgram(prog driver.Program) {
	b.bindPipeline(prog.(*Pipeline), vk.PIPELINE_BIND_POINT_COMPUTE)
}

func (b *Backend) bindPipeline(p *Pipeline, point vk.PipelineBindPoint) {
	b.pipe = p
	b.desc.dirty = p.desc.descLayout != 0
	cmdBuf := b.currentCmdBuf()
	vk.CmdBindPipeline(cmdBuf, point, p.pipe)
}

func (s *Shader) Release() {
	vk.DestroyShaderModule(s.dev, s.module)
	*s = Shader{}
}

func (b *Backend) BindStorageBuffer(binding int, buffer driver.Buffer) {
	buf := buffer.(*Buffer)
	b.desc.bufBinds[binding] = buf
	b.desc.dirty = true
	buf.barrier(b.currentCmdBuf(),
		vk.PIPELINE_STAGE_COMPUTE_SHADER_BIT,
		vk.ACCESS_SHADER_READ_BIT|vk.ACCESS_SHADER_WRITE_BIT,
	)
}

func (b *Backend) BindUniforms(buffer driver.Buffer) {
	buf := buffer.(*Buffer)
	cmdBuf := b.currentCmdBuf()
	for _, s := range b.pipe.pushRanges {
		off := vk.PushConstantRangeOffset(s)
		vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, vk.PushConstantRangeStageFlags(s), off, buf.store[off:off+vk.PushConstantRangeSize(s)])
	}
}

func (b *Backend) BindVertexBuffer(buffer driver.Buffer, offset int) {
	buf := buffer.(*Buffer)
	cmdBuf := b.currentCmdBuf()
	b.bindings = b.bindings[:0]
	b.offsets = b.offsets[:0]
	for i := 0; i < b.pipe.ninputs; i++ {
		b.bindings = append(b.bindings, buf.buf)
		b.offsets = append(b.offsets, vk.DeviceSize(offset))
	}
	vk.CmdBindVertexBuffers(cmdBuf, 0, b.bindings, b.offsets)
}

func (b *Backend) BindIndexBuffer(buffer driver.Buffer) {
	buf := buffer.(*Buffer)
	cmdBuf := b.currentCmdBuf()
	vk.CmdBindIndexBuffer(cmdBuf, buf.buf, 0, vk.INDEX_TYPE_UINT16)
}

func (b *Buffer) Download(data []byte) error {
	if b.buf == 0 {
		copy(data, b.store)
		return nil
	}
	stage, mem, off := b.backend.stagingBuffer(len(data))
	cmdBuf := b.backend.ensureCmdBuf()
	b.barrier(cmdBuf,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_READ_BIT,
	)
	vk.CmdCopyBuffer(cmdBuf, b.buf, stage.buf, 0, off, len(data))
	stage.scope.stage = vk.PIPELINE_STAGE_TRANSFER_BIT
	stage.scope.access = vk.ACCESS_TRANSFER_WRITE_BIT
	stage.barrier(cmdBuf,
		vk.PIPELINE_STAGE_HOST_BIT,
		vk.ACCESS_HOST_READ_BIT,
	)
	b.backend.submitCmdBuf(b.backend.fence)
	vk.WaitForFences(b.backend.dev, b.backend.fence)
	vk.ResetFences(b.backend.dev, b.backend.fence)
	copy(data, mem)
	return nil
}

func (b *Buffer) Upload(data []byte) {
	if b.buf == 0 {
		copy(b.store, data)
		return
	}
	stage, mem, off := b.backend.stagingBuffer(len(data))
	copy(mem, data)
	cmdBuf := b.backend.ensureCmdBuf()
	b.barrier(cmdBuf,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_WRITE_BIT,
	)
	vk.CmdCopyBuffer(cmdBuf, stage.buf, b.buf, off, 0, len(data))
	var access vk.AccessFlags
	if b.usage&vk.BUFFER_USAGE_INDEX_BUFFER_BIT != 0 {
		access |= vk.ACCESS_INDEX_READ_BIT
	}
	if b.usage&vk.BUFFER_USAGE_VERTEX_BUFFER_BIT != 0 {
		access |= vk.ACCESS_VERTEX_ATTRIBUTE_READ_BIT
	}
	if access != 0 {
		b.barrier(cmdBuf,
			vk.PIPELINE_STAGE_VERTEX_INPUT_BIT,
			access,
		)
	}
}

func (b *Buffer) barrier(cmdBuf vk.CommandBuffer, stage vk.PipelineStageFlags, access vk.AccessFlags) {
	srcStage := b.scope.stage
	if srcStage == 0 {
		b.scope.stage = stage
		b.scope.access = access
		return
	}
	barrier := vk.BuildBufferMemoryBarrier(
		b.buf,
		b.scope.access, access,
	)
	vk.CmdPipelineBarrier(cmdBuf, srcStage, stage, vk.DEPENDENCY_BY_REGION_BIT, nil, []vk.BufferMemoryBarrier{barrier}, nil)
	b.scope.stage = stage
	b.scope.access = access
}

func (b *Buffer) Release() {
	freeb := *b
	if freeb.buf != 0 {
		b.backend.deferFunc(func(d vk.Device) {
			vk.DestroyBuffer(d, freeb.buf)
			vk.FreeMemory(d, freeb.mem)
		})
	}
	*b = Buffer{}
}

func (t *Texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error {
	if len(pixels) == 0 {
		return nil
	}
	sz := src.Size()
	stageStride := sz.X * 4
	n := sz.Y * stageStride
	stage, mem, off := t.backend.stagingBuffer(n)
	cmdBuf := t.backend.ensureCmdBuf()
	region := vk.BuildBufferImageCopy(off, stageStride/4, src.Min.X, src.Min.Y, sz.X, sz.Y)
	t.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
		vk.PIPELINE_STAGE_TRANSFER_BIT,
		vk.ACCESS_TRANSFER_READ_BIT,
	)
	vk.CmdCopyImageToBuffer(cmdBuf, t.img, t.layout, stage.buf, []vk.BufferImageCopy{region})
	stage.scope.stage = vk.PIPELINE_STAGE_TRANSFER_BIT
	stage.scope.access = vk.ACCESS_TRANSFER_WRITE_BIT
	stage.barrier(cmdBuf,
		vk.PIPELINE_STAGE_HOST_BIT,
		vk.ACCESS_HOST_READ_BIT,
	)
	t.backend.submitCmdBuf(t.backend.fence)
	vk.WaitForFences(t.backend.dev, t.backend.fence)
	vk.ResetFences(t.backend.dev, t.backend.fence)
	var srcOff, dstOff int
	for y := 0; y < sz.Y; y++ {
		dstRow := pixels[srcOff : srcOff+stageStride]
		srcRow := mem[dstOff : dstOff+stageStride]
		copy(dstRow, srcRow)
		dstOff += stageStride
		srcOff += stride
	}
	return nil
}

func (b *Backend) currentCmdBuf() vk.CommandBuffer {
	cur := b.cmdPool.current
	if cur == nil {
		panic("vulkan: invalid operation outside a render or compute pass")
	}
	return cur
}

func (b *Backend) ensureCmdBuf() vk.CommandBuffer {
	if b.cmdPool.current != nil {
		return b.cmdPool.current
	}
	if b.cmdPool.used < len(b.cmdPool.buffers) {
		buf := b.cmdPool.buffers[b.cmdPool.used]
		b.cmdPool.current = buf
	} else {
		buf, err := vk.AllocateCommandBuffer(b.dev, b.cmdPool.pool)
		if err != nil {
			panic(err)
		}
		b.cmdPool.buffers = append(b.cmdPool.buffers, buf)
		b.cmdPool.current = buf
	}
	b.cmdPool.used++
	buf := b.cmdPool.current
	if err := vk.BeginCommandBuffer(buf); err != nil {
		panic(err)
	}
	return buf
}

func (b *Backend) BeginRenderPass(tex driver.Texture, d driver.LoadDesc) {
	t := tex.(*Texture)
	var vkop vk.AttachmentLoadOp
	switch d.Action {
	case driver.LoadActionClear:
		vkop = vk.ATTACHMENT_LOAD_OP_CLEAR
	case driver.LoadActionInvalidate:
		vkop = vk.ATTACHMENT_LOAD_OP_DONT_CARE
	case driver.LoadActionKeep:
		vkop = vk.ATTACHMENT_LOAD_OP_LOAD
	}
	cmdBuf := b.ensureCmdBuf()
	if sem := t.acquire; sem != 0 {
		// The render pass targets a framebuffer that has an associated acquire semaphore.
		// Wait for it by forming an execution barrier.
		b.waitSems = append(b.waitSems, sem)
		b.waitStages = append(b.waitStages, vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT)
		// But only for the first pass in a frame.
		t.acquire = 0
	}
	t.imageBarrier(cmdBuf,
		vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
		vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
		vk.ACCESS_COLOR_ATTACHMENT_READ_BIT|vk.ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
	)
	pass := b.lookupPass(t.format, vkop, t.layout, t.passLayout)
	col := d.ClearColor
	vk.CmdBeginRenderPass(cmdBuf, pass, t.fbo, t.width, t.height, [4]float32{col.R, col.G, col.B, col.A})
	t.layout = t.passLayout
	// If the render pass describes an automatic image layout transition to its final layout, there
	// is an implicit image barrier with destination PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT. Make
	// sure any subsequent barrier includes the transition.
	// See also https://www.khronos.org/registry/vulkan/specs/1.0/html/vkspec.html#VkSubpassDependency.
	t.scope.stage |= vk.PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT
}

func (b *Backend) EndRenderPass() {
	vk.CmdEndRenderPass(b.cmdPool.current)
}

func (b *Backend) BeginCompute() {
	b.ensureCmdBuf()
}

func (b *Backend) EndCompute() {
}

func (b *Backend) lookupPass(fmt vk.Format, loadAct vk.AttachmentLoadOp, initLayout, finalLayout vk.ImageLayout) vk.RenderPass {
	key := passKey{fmt: fmt, loadAct: loadAct, initLayout: initLayout, finalLayout: finalLayout}
	if pass, ok := b.passes[key]; ok {
		return pass
	}
	pass, err := vk.CreateRenderPass(b.dev, fmt, loadAct, initLayout, finalLayout, nil)
	if err != nil {
		panic(err)
	}
	b.passes[key] = pass
	return pass
}

func (b *Backend) submitCmdBuf(fence vk.Fence) {
	buf := b.cmdPool.current
	if buf == nil && fence == 0 {
		return
	}
	buf = b.ensureCmdBuf()
	b.cmdPool.current = nil
	if err := vk.EndCommandBuffer(buf); err != nil {
		panic(err)
	}
	if err := vk.QueueSubmit(b.queue, buf, b.waitSems, b.waitStages, b.sigSems, fence); err != nil {
		panic(err)
	}
	b.waitSems = b.waitSems[:0]
	b.sigSems = b.sigSems[:0]
	b.waitStages = b.waitStages[:0]
}

func (b *Backend) stagingBuffer(size int) (*Buffer, []byte, int) {
	if b.staging.size+size > b.staging.cap {
		if b.staging.buf != nil {
			vk.UnmapMemory(b.dev, b.staging.buf.mem)
			b.staging.buf.Release()
			b.staging.cap = 0
		}
		cap := 2 * (b.staging.size + size)
		buf, err := b.newBuffer(cap, vk.BUFFER_USAGE_TRANSFER_SRC_BIT|vk.BUFFER_USAGE_TRANSFER_DST_BIT,
			vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT|vk.MEMORY_PROPERTY_HOST_COHERENT_BIT)
		if err != nil {
			panic(err)
		}
		mem, err := vk.MapMemory(b.dev, buf.mem, 0, cap)
		if err != nil {
			buf.Release()
			panic(err)
		}
		b.staging.buf = buf
		b.staging.mem = mem
		b.staging.size = 0
		b.staging.cap = cap
	}
	off := b.staging.size
	b.staging.size += size
	mem := b.staging.mem[off : off+size]
	return b.staging.buf, mem, off
}

func formatFor(format driver.TextureFormat) vk.Format {
	switch format {
	case driver.TextureFormatRGBA8:
		return vk.FORMAT_R8G8B8A8_UNORM
	case driver.TextureFormatSRGBA:
		return vk.FORMAT_R8G8B8A8_SRGB
	case driver.TextureFormatFloat:
		return vk.FORMAT_R16_SFLOAT
	default:
		panic("unsupported texture format")
	}
}

func mapErr(err error) error {
	var vkErr vk.Error
	if errors.As(err, &vkErr) && vkErr == vk.ERROR_DEVICE_LOST {
		return driver.ErrDeviceLost
	}
	return err
}

func (f *Texture) ImplementsRenderTarget() {}
