/* Copyright (c) 2020 Themaister
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#define NOMINMAX
#include "video_interface.hpp"
#include "rdp_renderer.hpp"
#include "luts.hpp"
#include "bitops.hpp"
#include <cmath>

#ifndef PARALLEL_RDP_SHADER_DIR
#include "shaders/slangmosh.hpp"
#endif

namespace RDP
{
void VideoInterface::set_device(Vulkan::Device *device_)
{
	device = device_;
	init_gamma_table();

	if (const char *env = getenv("VI_DEBUG"))
		debug_channel = strtol(env, nullptr, 0) != 0;
	if (const char *env = getenv("VI_DEBUG_X"))
		filter_debug_channel_x = strtol(env, nullptr, 0);
	if (const char *env = getenv("VI_DEBUG_Y"))
		filter_debug_channel_y = strtol(env, nullptr, 0);

	if (const char *timestamp_env = getenv("PARALLEL_RDP_BENCH"))
		timestamp = strtol(timestamp_env, nullptr, 0) > 0;
}

void VideoInterface::set_renderer(Renderer *renderer_)
{
	renderer = renderer_;
}

int VideoInterface::resolve_shader_define(const char *name, const char *define) const
{
	if (strcmp(define, "DEBUG_ENABLE") == 0)
		return int(debug_channel);
	else
		return 0;
}

void VideoInterface::message(const std::string &tag, uint32_t code, uint32_t x, uint32_t y, uint32_t, uint32_t num_words,
                             const Vulkan::DebugChannelInterface::Word *words)
{
	if (filter_debug_channel_x >= 0 && x != uint32_t(filter_debug_channel_x))
		return;
	if (filter_debug_channel_y >= 0 && y != uint32_t(filter_debug_channel_y))
		return;

	switch (num_words)
	{
	case 1:
		LOGI("(%u, %u), line %d.\n", x, y, words[0].s32);
		break;

	case 2:
		LOGI("(%u, %u), line %d: (%d).\n", x, y, words[0].s32, words[1].s32);
		break;

	case 3:
		LOGI("(%u, %u), line %d: (%d, %d).\n", x, y, words[0].s32, words[1].s32, words[2].s32);
		break;

	case 4:
		LOGI("(%u, %u), line %d: (%d, %d, %d).\n", x, y,
		     words[0].s32, words[1].s32, words[2].s32, words[3].s32);
		break;

	default:
		LOGE("Unknown number of generic parameters: %u\n", num_words);
		break;
	}
}

void VideoInterface::init_gamma_table()
{
	Vulkan::BufferCreateInfo info = {};
	info.domain = Vulkan::BufferDomain::Device;
	info.size = sizeof(gamma_table);
	info.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;

	gamma_lut = device->create_buffer(info, gamma_table);

	Vulkan::BufferViewCreateInfo view = {};
	view.buffer = gamma_lut.get();
	view.range = sizeof(gamma_table);
	view.format = VK_FORMAT_R8_UINT;
	gamma_lut_view = device->create_buffer_view(view);
}

void VideoInterface::set_vi_register(VIRegister reg, uint32_t value)
{
	vi_registers[unsigned(reg)] = value;
}

void VideoInterface::set_rdram(const Vulkan::Buffer *rdram_, size_t offset, size_t size)
{
	rdram = rdram_;
	rdram_offset = offset;
	rdram_size = size;
}

void VideoInterface::set_hidden_rdram(const Vulkan::Buffer *hidden_rdram_)
{
	hidden_rdram = hidden_rdram_;
}

void VideoInterface::set_shader_bank(const ShaderBank *bank)
{
	shader_bank = bank;
}

static VkPipelineStageFlagBits2 layout_to_stage(VkImageLayout layout)
{
	switch (layout)
	{
	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
		// We use BLIT stage internally, but caller expects COPY generally.
		return VK_PIPELINE_STAGE_2_BLIT_BIT | VK_PIPELINE_STAGE_2_COPY_BIT;

	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
		return VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;

	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
		return VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;

	default:
		return 0;
	}
}

static VkAccessFlags2 layout_to_access(VkImageLayout layout)
{
	switch (layout)
	{
	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
		return VK_ACCESS_TRANSFER_READ_BIT;

	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
		return VK_ACCESS_TRANSFER_WRITE_BIT;

	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
		return VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;

	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
		return VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

	default:
		return 0;
	}
}

VideoInterface::Registers VideoInterface::decode_vi_registers(HorizontalInfoLines *lines) const
{
	Registers reg = {};

	// Definitely should not change per scanline ...
	reg.status = vi_registers[unsigned(VIRegister::Control)];
	reg.vi_width = vi_registers[unsigned(VIRegister::Width)] & 0xfff;
	reg.vi_offset = vi_registers[unsigned(VIRegister::Origin)] & 0xffffff;
	reg.v_current_line = vi_registers[unsigned(VIRegister::VCurrentLine)] & 1;
	reg.v_start = (vi_registers[unsigned(VIRegister::VStart)] >> 16) & 0x3ff;

	// It might be possible to change YStart/YAdd per scanline, but it's dubious ...
	int y_start = (vi_registers[unsigned(VIRegister::YScale)] >> 16) & 0xfff;
	int y_add = vi_registers[unsigned(VIRegister::YScale)] & 0xfff;
	reg.init_y_add = y_add;

	// Must be constant for a frame, or things won't make any sense.
	int v_end = vi_registers[unsigned(VIRegister::VStart)] & 0x3ff;
	int v_sync = vi_registers[unsigned(VIRegister::VSync)] & 0x3ff;

	reg.is_pal = unsigned(v_sync) > (VI_V_SYNC_NTSC + 25);

	// Clamp vertical ranges.
	// Restrict the area we might have to scan out to AA buffers as a performance optimization
	// and safety since we iterate over v_start/v_end later.
	int v_end_max = reg.is_pal ? VI_V_END_PAL : VI_V_END_NTSC;
	if (v_end > v_end_max)
		v_end = v_end_max;
	if (reg.v_start > v_end_max)
		reg.v_start = v_end_max;

	int v_start_offset = reg.is_pal ? VI_V_OFFSET_PAL : VI_V_OFFSET_NTSC;
	reg.v_res = (v_end - reg.v_start) >> 1;
	reg.v_start = (reg.v_start - v_start_offset) / 2;

	if (reg.v_start < 0)
	{
		// If YAdd can change per scanline, this won't be correct.
		y_start -= y_add * reg.v_start;
		// v_res is not adjusted here for some reason, but h_res is?
		reg.v_start = 0;
	}

	// Clamp v_res to scanout range.
	reg.v_res = std::min(reg.v_res, int(VI_MAX_OUTPUT_SCANLINES) - reg.v_start);

	// Horizontal shenanigans.
	int h_start_clamp_lo = INT32_MAX;
	int h_end_clamp_hi = 0;
	int h_start_lo = INT32_MAX;
	int h_end_hi = 0;

	reg.max_x = 0;
	bool degenerate_y = reg.v_res <= 0;

	// Clear out degenerate lines.
	if (lines)
	{
		if (degenerate_y)
		{
			for (auto &line : lines->lines)
				line = {};
		}
		else
		{
			for (int line = 0; line < reg.v_start; line++)
				lines->lines[line] = {};
			for (int line = reg.v_start + reg.v_res; line < int(VI_MAX_OUTPUT_SCANLINES); line++)
				lines->lines[line] = {};
		}
	}

	const auto analyze_line = [&](int x_start, int x_add, int h_start, int h_end, HorizontalInfo *line) {
		// Clamp horizontal region to [0, 640].
		bool left_clamp = false;
		bool right_clamp = false;

		h_start -= reg.is_pal ? VI_H_OFFSET_PAL : VI_H_OFFSET_NTSC;
		h_end -= reg.is_pal ? VI_H_OFFSET_PAL : VI_H_OFFSET_NTSC;

		if (h_start < 0)
		{
			x_start -= x_add * h_start;
			h_start = 0;
			// Reference weirdness that doesn't really make sense.
			left_clamp = true;
		}

		if (h_end > VI_SCANOUT_WIDTH)
		{
			h_end = VI_SCANOUT_WIDTH;
			// Reference weirdness that doesn't really make sense.
			right_clamp = true;
		}

		int h_start_clamp = h_start + (left_clamp ? 0 : 8);
		int h_end_clamp = h_end - (right_clamp ? 0 : 7);

		// Effectively, these are bounding boxes.
		int h_res = h_end - h_start;
		int max_x = (x_start + h_res * x_add) >> 10;
		reg.max_x = std::max(reg.max_x, max_x);
		h_start_lo = std::min(h_start_lo, h_start);
		h_end_hi = std::max(h_end_hi, h_end);
		h_start_clamp_lo = std::min(h_start_clamp_lo, h_start_clamp);
		h_end_clamp_hi = std::max(h_end_clamp_hi, h_end_clamp);

		if (line)
		{
			auto &l = *line;
			l.h_start = h_start;
			l.h_start_clamp = h_start_clamp;
			l.h_end_clamp = h_end_clamp;
			l.x_start = x_start;
			l.x_add = x_add;
			l.y_start = y_start;
			l.y_add = y_add;
			l.y_base = 0; // TODO: If we start adjusting YAdd per scanline, we'll need to begin a new base region.
		}
	};

	if (degenerate_y || !per_line_state.ended)
	{
		int x_start = (vi_registers[unsigned(VIRegister::XScale)] >> 16) & 0xfff;
		int x_add = vi_registers[unsigned(VIRegister::XScale)] & 0xfff;
		int h_start = (vi_registers[unsigned(VIRegister::HStart)] >> 16) & 0x3ff;
		int h_end = vi_registers[unsigned(VIRegister::HStart)] & 0x3ff;

		HorizontalInfo line_info;

		// Need to make sure we update bounding box state for X at least.
		// This is treated as a degenerate frame, not necessarily invalid frame (null handle).
		// This is to have same behavior as reference.
		analyze_line(x_start, x_add, h_start, h_end, &line_info);

		if (lines)
			for (int line = reg.v_start; line < reg.v_start + reg.v_res; line++)
				lines->lines[line] = line_info;
	}
	else
	{
		for (int line = reg.v_start; line < reg.v_start + reg.v_res; line++)
		{
			// TODO: No idea if this is correct. This intuitively makes sense, but that's about it.
			int effective_line = 2 * line + v_start_offset + int(reg.v_current_line == 0);

			int x_start = (per_line_state.x_scale.line_state[effective_line] >> 16) & 0xfff;
			int x_add = per_line_state.x_scale.line_state[effective_line] & 0xfff;
			int h_start = (per_line_state.h_start.line_state[effective_line] >> 16) & 0x3ff;
			int h_end = per_line_state.h_start.line_state[effective_line] & 0x3ff;

			analyze_line(x_start, x_add, h_start, h_end, lines ? &lines->lines[line] : nullptr);
		}
	}

	// Effectively, these are bounding boxes.
	reg.max_y = (y_start + reg.v_res * y_add) >> 10;
	reg.h_start = h_start_lo;
	reg.h_res = h_end_hi - h_start_lo;
	reg.h_start_clamp = h_start_clamp_lo;
	reg.h_res_clamp = h_end_clamp_hi - h_start_clamp_lo;

	// The basic formula is that a frame is counted with an active horizontal range of
	// X(range) = [H_OFFSET, H_OFFSET + H_RES], giving 640 output pixels per line.
	// Similarly, vertical scanout has an active range of Y(range) = [V_OFFSET, V_OFFSET + V_RES].
	// Y is counted in terms of interlaced lines (i.e. 480 and 576).
	// We will scan out half of these per field (e.g. 240p or 480i for NTSC).
	// The HStart and VStart registers are used to signal where on screen we render.
	// HStart and VStart registers might carve out a portion of the screen, or use a larger one,
	// the active area on screen is an intersection of the VI register state and the X(range)/Y(range).
	// When the X counter hits HStart, we begin computing the X coordinate we want to sample based on
	// X(sample) = XStart + XAdd * (X - HStart).
	// Similarly, Y(sample) = YStart + YAdd * (Y - (VStart >> 1)), YAdd increments once per scanline.
	// We always normalize the interpolations to be progressive.
	// Interlacing just shifts positions on screen after the fact.
	//
	// VRAM(X, Y) is fetched with any post-processing required, looking at neighboring VRAM pixels.
	// For this reason, we compute the maximum X and Y we might access, and build an X x Y image
	// which is already preprocessed with AA, Divot filters, etc.
	// The final scaling pass interpolates that result.
	// The mental model here is that the VI could have a line buffer to keep some scanlines in cache to support the
	// processing. XStart/YStart registers just control how fast we iterate through these lines,
	// which implements scaling effects.
	//
	// As another weird quirk, it seems like we need to account for a
	// 8 pixel guard band horizontally (reg.left_clamp / reg.right_clamp)
	// if we begin scanout inside the active region for whatever reason.
	// This is to match reference.

	return reg;
}

void VideoInterface::scanout_memory_range(unsigned &offset, unsigned &length) const
{
	auto reg = decode_vi_registers(nullptr);

	bool divot = (reg.status & VI_CONTROL_DIVOT_ENABLE_BIT) != 0;

	// Need to sample a 2-pixel border to have room for AA filter and divot.
	int aa_width = reg.max_x + 2 + 4 + int(divot) * 2;
	// 1 pixel border on top and bottom.
	int aa_height = reg.max_y + 1 + 4;

	int x_off = divot ? -3 : -2;
	int y_off = -2;

	if (reg.vi_offset == 0 || reg.h_res <= 0 || reg.h_start >= VI_SCANOUT_WIDTH)
	{
		offset = 0;
		length = 0;
		return;
	}

	int pixel_size = ((reg.status & VI_CONTROL_TYPE_MASK) | VI_CONTROL_TYPE_RGBA5551_BIT) == VI_CONTROL_TYPE_RGBA8888_BIT ? 4 : 2;
	reg.vi_offset &= ~(pixel_size - 1);
	reg.vi_offset += (x_off + y_off * reg.vi_width) * pixel_size;

	offset = reg.vi_offset;
	length = (aa_height * reg.vi_width + aa_width) * pixel_size;
}

bool VideoInterface::need_fetch_bug_emulation(const Registers &regs, unsigned scaling_factor)
{
	// If we risk sampling same Y coordinate for two scanlines we can trigger this case,
	// so add workaround paths for it.
	return regs.init_y_add < 1024 && scaling_factor == 1;
}

Vulkan::ImageHandle VideoInterface::vram_fetch_stage(const Registers &regs, unsigned scaling_factor) const
{
	auto async_cmd = device->request_command_buffer(Vulkan::CommandBuffer::Type::AsyncCompute);
	Vulkan::ImageHandle vram_image;
	Vulkan::QueryPoolHandle start_ts, end_ts;
	bool divot = (regs.status & VI_CONTROL_DIVOT_ENABLE_BIT) != 0;

	if (scaling_factor > 1)
	{
		unsigned pixel_size_log2 = ((regs.status & VI_CONTROL_TYPE_MASK) == VI_CONTROL_TYPE_RGBA8888_BIT) ? 2 : 1;
		unsigned offset, length;
		scanout_memory_range(offset, length);
		renderer->submit_update_upscaled_domain_external(*async_cmd, offset, length, pixel_size_log2);
		async_cmd->barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
		                   VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
	}

	if (timestamp)
		start_ts = async_cmd->write_timestamp(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);

	// Need to sample a 2-pixel border to have room for AA filter and divot.
	int extract_width = regs.max_x + 2 + 4 + int(divot) * 2;
	// 1 pixel border on top and bottom.
	int extract_height = regs.max_y + 1 + 4;

	Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(
			extract_width,
			extract_height,
			VK_FORMAT_R8G8B8A8_UINT);

	rt_info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
	rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
	rt_info.misc = Vulkan::IMAGE_MISC_CONCURRENT_QUEUE_GRAPHICS_BIT |
	               Vulkan::IMAGE_MISC_CONCURRENT_QUEUE_ASYNC_COMPUTE_BIT;
	vram_image = device->create_image(rt_info);
	vram_image->set_layout(Vulkan::Layout::General);

	async_cmd->image_barrier(*vram_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL,
	                         0, 0, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT);

#ifdef PARALLEL_RDP_SHADER_DIR
	async_cmd->set_program("rdp://extract_vram.comp");
#else
	async_cmd->set_program(shader_bank->extract_vram);
#endif
	async_cmd->set_storage_texture(0, 0, vram_image->get_view());

	if (scaling_factor > 1)
	{
		async_cmd->set_storage_buffer(0, 1, *renderer->get_upscaled_rdram_buffer());
		async_cmd->set_storage_buffer(0, 2, *renderer->get_upscaled_hidden_rdram_buffer());
	}
	else
	{
		async_cmd->set_storage_buffer(0, 1, *rdram, rdram_offset, rdram_size);
		async_cmd->set_storage_buffer(0, 2, *hidden_rdram);
	}

	struct Push
	{
		uint32_t fb_offset;
		uint32_t fb_width;
		int32_t x_offset;
		int32_t y_offset;
		int32_t x_res;
		int32_t y_res;
	} push = {};

	if ((regs.status & VI_CONTROL_TYPE_MASK) == VI_CONTROL_TYPE_RGBA8888_BIT)
		push.fb_offset = regs.vi_offset >> 2;
	else
		push.fb_offset = regs.vi_offset >> 1;

	push.fb_width = regs.vi_width;
	push.x_offset = divot ? -3 : -2;
	push.y_offset = -2;
	push.x_res = extract_width;
	push.y_res = extract_height;

	async_cmd->set_specialization_constant_mask(7);
	async_cmd->set_specialization_constant(0, uint32_t(rdram_size));
	async_cmd->set_specialization_constant(1, regs.status & (VI_CONTROL_TYPE_MASK | VI_CONTROL_META_AA_BIT));
	async_cmd->set_specialization_constant(2, Util::trailing_zeroes(scaling_factor));

	async_cmd->push_constants(&push, 0, sizeof(push));
	async_cmd->dispatch((extract_width + 15) / 16,
	                    (extract_height + 7) / 8,
	                    1);

	// Just enforce an execution barrier here for rendering work in next frame.
	async_cmd->barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0,
	                   VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0);

	if (timestamp)
	{
		end_ts = async_cmd->write_timestamp(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
		device->register_time_interval("VI GPU", std::move(start_ts), std::move(end_ts), "extract-vram");
	}

	Vulkan::Semaphore sem;
	device->submit(async_cmd, nullptr, 1, &sem);
	device->add_wait_semaphore(Vulkan::CommandBuffer::Type::Generic, std::move(sem),
	                           VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, true);

	return vram_image;
}

Vulkan::ImageHandle VideoInterface::aa_fetch_stage(Vulkan::CommandBuffer &cmd, Vulkan::Image &vram_image,
                                                   const Registers &regs, unsigned scaling_factor) const
{
	Vulkan::ImageHandle aa_image;
	Vulkan::QueryPoolHandle start_ts, end_ts;
	bool fetch_bug = need_fetch_bug_emulation(regs, scaling_factor);
	bool divot = (regs.status & VI_CONTROL_DIVOT_ENABLE_BIT) != 0;

	// For the AA pass, we need to figure out how many pixels we might need to read.
	int aa_width = regs.max_x + 3 + int(divot) * 2;
	int aa_height = regs.max_y + 2;

	Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(aa_width, aa_height,
	                                                                         VK_FORMAT_R8G8B8A8_UINT);
	rt_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
	rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
	rt_info.layers = fetch_bug ? 2 : 1;
	rt_info.misc = Vulkan::IMAGE_MISC_FORCE_ARRAY_BIT;
	aa_image = device->create_image(rt_info);

	Vulkan::ImageViewCreateInfo view_info = {};
	view_info.image = aa_image.get();
	view_info.view_type = VK_IMAGE_VIEW_TYPE_2D;
	view_info.layers = 1;

	Vulkan::ImageViewHandle aa_primary, aa_secondary;
	view_info.base_layer = 0;
	aa_primary = device->create_image_view(view_info);

	if (fetch_bug)
	{
		view_info.base_layer = 1;
		aa_secondary = device->create_image_view(view_info);
	}

	Vulkan::RenderPassInfo rp;
	rp.color_attachments[0] = aa_primary.get();
	rp.clear_attachments = 0;

	if (fetch_bug)
	{
		rp.color_attachments[1] = aa_secondary.get();
		rp.num_color_attachments = 2;
		rp.store_attachments = 3;
	}
	else
	{
		rp.num_color_attachments = 1;
		rp.store_attachments = 1;
	}

	cmd.image_barrier(*aa_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
	                  0, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);

	if (timestamp)
		start_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);

	cmd.begin_render_pass(rp);
	cmd.set_opaque_state();

#ifdef PARALLEL_RDP_SHADER_DIR
	cmd.set_program("rdp://fullscreen.vert", "rdp://vi_fetch.frag",
	                {
			                { "DEBUG_ENABLE", debug_channel ? 1 : 0 },
			                { "FETCH_BUG", fetch_bug ? 1 : 0 },
	                });
#else
	cmd.set_program(device->request_program(shader_bank->fullscreen, shader_bank->vi_fetch[int(fetch_bug)]));
#endif

	struct Push
	{
		int32_t x_offset;
		int32_t y_offset;
	} push = {};

	push.x_offset = 2;
	push.y_offset = 2;

	cmd.push_constants(&push, 0, sizeof(push));

	cmd.set_specialization_constant_mask(3);
	cmd.set_specialization_constant(0, uint32_t(rdram_size));
	cmd.set_specialization_constant(1,
	                                regs.status & (VI_CONTROL_META_AA_BIT | VI_CONTROL_DITHER_FILTER_ENABLE_BIT));

	cmd.set_texture(0, 0, vram_image.get_view());
	cmd.draw(3);
	cmd.end_render_pass();

	if (timestamp)
	{
		end_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
		device->register_time_interval("VI GPU", std::move(start_ts), std::move(end_ts), "vi-fetch");
	}

	cmd.image_barrier(*aa_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
	                  VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
	                  VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);

	return aa_image;
}

Vulkan::ImageHandle VideoInterface::divot_stage(Vulkan::CommandBuffer &cmd, Vulkan::Image &aa_image,
                                                const Registers &regs, unsigned scaling_factor) const
{
	Vulkan::ImageHandle divot_image;
	Vulkan::QueryPoolHandle start_ts, end_ts;
	bool fetch_bug = need_fetch_bug_emulation(regs, scaling_factor);

	// For the divot pass, we need to figure out how many pixels we might need to read.
	int divot_width = regs.max_x + 2;
	int divot_height = regs.max_y + 2;

	Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(divot_width, divot_height,
	                                                                         VK_FORMAT_R8G8B8A8_UINT);
	rt_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
	rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
	rt_info.layers = fetch_bug ? 2 : 1;
	rt_info.misc = Vulkan::IMAGE_MISC_FORCE_ARRAY_BIT;
	divot_image = device->create_image(rt_info);

	Vulkan::ImageViewCreateInfo view_info = {};
	view_info.image = divot_image.get();
	view_info.view_type = VK_IMAGE_VIEW_TYPE_2D;
	view_info.layers = 1;

	Vulkan::ImageViewHandle divot_primary, divot_secondary;
	view_info.base_layer = 0;
	divot_primary = device->create_image_view(view_info);

	if (fetch_bug)
	{
		view_info.base_layer = 1;
		divot_secondary = device->create_image_view(view_info);
	}

	Vulkan::RenderPassInfo rp;
	rp.color_attachments[0] = divot_primary.get();
	rp.clear_attachments = 0;

	if (fetch_bug)
	{
		rp.color_attachments[1] = divot_secondary.get();
		rp.num_color_attachments = 2;
		rp.store_attachments = 3;
	}
	else
	{
		rp.num_color_attachments = 1;
		rp.store_attachments = 1;
	}

	cmd.image_barrier(*divot_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
	                  0, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);

	if (timestamp)
		start_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);

	cmd.begin_render_pass(rp);
	cmd.set_opaque_state();

#ifdef PARALLEL_RDP_SHADER_DIR
	cmd.set_program("rdp://fullscreen.vert", "rdp://vi_divot.frag", {
			{ "DEBUG_ENABLE", debug_channel ? 1 : 0 },
			{ "FETCH_BUG", fetch_bug ? 1 : 0 },
	});
#else
	cmd.set_program(device->request_program(shader_bank->fullscreen, shader_bank->vi_divot[int(fetch_bug)]));
#endif

	cmd.set_texture(0, 0, aa_image.get_view());
	cmd.draw(3);
	cmd.end_render_pass();

	if (timestamp)
	{
		end_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
		device->register_time_interval("VI GPU", std::move(start_ts), std::move(end_ts), "vi-divot");
	}

	cmd.image_barrier(*divot_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
	                  VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
	                  VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);

	return divot_image;
}

void VideoInterface::bind_horizontal_info_view(Vulkan::CommandBuffer &cmd, const HorizontalInfoLines &lines)
{
	auto &device = cmd.get_device();

	Vulkan::BufferCreateInfo horizontal_buffer_info = {};
	horizontal_buffer_info.size = sizeof(lines);
	horizontal_buffer_info.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
	horizontal_buffer_info.domain = Vulkan::BufferDomain::LinkedDeviceHost;
	auto scanout_parameters = device.create_buffer(horizontal_buffer_info, &lines);

	Vulkan::BufferViewCreateInfo horizontal_view_info = {};
	horizontal_view_info.format = VK_FORMAT_R32G32B32A32_SINT;
	horizontal_view_info.buffer = scanout_parameters.get();
	horizontal_view_info.range = sizeof(lines);

	auto scanout_parameters_view = device.create_buffer_view(horizontal_view_info);
	cmd.set_buffer_view(0, 1, *scanout_parameters_view);
}

Vulkan::ImageHandle VideoInterface::scale_stage(Vulkan::CommandBuffer &cmd, const Vulkan::Image *divot_image,
                                                Registers regs, const HorizontalInfoLines &lines,
                                                unsigned scaling_factor, bool degenerate,
                                                const ScanoutOptions &options, bool final_pass) const
{
	Vulkan::ImageHandle scale_image;
	Vulkan::QueryPoolHandle start_ts, end_ts;
	bool fetch_bug = need_fetch_bug_emulation(regs, scaling_factor);
	bool serrate = (regs.status & VI_CONTROL_SERRATE_BIT) != 0 && !options.upscale_deinterlacing;

	Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(
			VI_SCANOUT_WIDTH * scaling_factor,
			((regs.is_pal ? VI_V_RES_PAL: VI_V_RES_NTSC) >> int(!serrate)) * scaling_factor,
			VK_FORMAT_R8G8B8A8_UNORM);

	unsigned crop_left = 0;
	unsigned crop_right = 0;
	unsigned crop_top = 0;
	unsigned crop_bottom = 0;

	if (options.crop_rect.enable)
	{
		crop_left = options.crop_rect.left;
		crop_right = options.crop_rect.right;
		crop_top = options.crop_rect.top;
		crop_bottom = options.crop_rect.bottom;

		if (serrate)
		{
			crop_top *= 2;
			crop_bottom *= 2;
		}
	}
	else
	{
		// Rescale crop pixels to preserve aspect ratio.
		auto crop_pixels_y = options.crop_overscan_pixels * (serrate ? 2 : 1);
		auto crop_pixels_x = unsigned(std::round(float(crop_pixels_y) * (float(rt_info.width) / float(rt_info.height))));

		crop_left = crop_right = crop_pixels_x;
		crop_top = crop_bottom = crop_pixels_y;
	}

	crop_left *= scaling_factor;
	crop_right *= scaling_factor;
	crop_top *= scaling_factor;
	crop_bottom *= scaling_factor;

	if (crop_left + crop_right < rt_info.width && crop_top + crop_bottom < rt_info.height)
	{
		rt_info.width -= crop_left + crop_right;
		rt_info.height -= crop_top + crop_bottom;
	}
	else
	{
		LOGE("Too large crop of %u x %u for RT %u x %u.\n",
			 crop_left + crop_right, crop_top + crop_bottom, rt_info.width, rt_info.height);
	}

	rt_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
	rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
	rt_info.misc = Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;

	if (options.export_scanout && final_pass)
	{
		rt_info.misc |= Vulkan::IMAGE_MISC_EXTERNAL_MEMORY_BIT;
		rt_info.external.memory_handle_type = options.export_handle_type;
	}

	scale_image = device->create_image(rt_info);

	if (!scale_image)
	{
		LOGE("Failed to allocate scale image.\n");
		return {};
	}

	Vulkan::RenderPassInfo rp;
	rp.color_attachments[0] = &scale_image->get_view();
	memset(&rp.clear_color[0], 0, sizeof(rp.clear_color[0]));
	rp.num_color_attachments = 1;
	rp.clear_attachments = 1;
	rp.store_attachments = 1;

	cmd.image_barrier(*scale_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
	                  0, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);

	if (prev_scanout_image)
	{
		if (prev_image_is_external)
		{
			cmd.acquire_image_barrier(*prev_scanout_image, prev_image_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
			                          VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
		}
		else if (prev_image_layout != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
		{
			VK_ASSERT(prev_image_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
			cmd.image_barrier(*prev_scanout_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
			                  VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_BLIT_BIT, 0,
			                  VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
		}
	}

	if (timestamp)
		start_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);

	cmd.begin_render_pass(rp);

	cmd.set_specialization_constant_mask((1 << 1) | (1 << 2));
	cmd.set_specialization_constant(1,
	                                regs.status & (VI_CONTROL_GAMMA_ENABLE_BIT |
	                                               VI_CONTROL_GAMMA_DITHER_ENABLE_BIT |
	                                               VI_CONTROL_META_SCALE_BIT |
	                                               VI_CONTROL_META_AA_BIT));
	cmd.set_specialization_constant(2, uint32_t(fetch_bug));

	struct Push
	{
		int32_t h_offset, v_offset;
		int32_t v_start;
		uint32_t y_add;
		uint32_t frame_count;

		uint32_t serrate_shift;
		uint32_t serrate_mask;
		uint32_t serrate_select;

		uint32_t info_y_shift;
	} push = {};

	push.info_y_shift = Util::floor_log2(scaling_factor);

	if (serrate)
	{
		regs.v_start *= 2;
		regs.v_res *= 2;
		push.serrate_shift = 1;
		push.serrate_mask = 1;
		bool field_state = regs.v_current_line == 0;
		push.serrate_select = int(field_state);
		push.info_y_shift++;
	}

	push.h_offset = int(crop_left);
	push.v_offset = int(crop_top);
	push.v_start = regs.v_start;
	push.y_add = regs.init_y_add;
	push.frame_count = frame_count;

	cmd.set_opaque_state();
#ifdef PARALLEL_RDP_SHADER_DIR
	cmd.set_program("rdp://fullscreen.vert", "rdp://vi_scale.frag", {
			{ "DEBUG_ENABLE", debug_channel ? 1 : 0 },
	});
#else
	cmd.set_program(device->request_program(shader_bank->fullscreen, shader_bank->vi_scale));
#endif
	cmd.set_buffer_view(1, 0, *gamma_lut_view);
	bind_horizontal_info_view(cmd, lines);

	cmd.push_constants(&push, 0, sizeof(push));

	const auto shift_rect = [](VkRect2D &rect, int x, int y) {
		rect.offset.x += x;
		rect.offset.y += y;

		if (rect.offset.x < 0)
		{
			rect.extent.width += rect.offset.x;
			rect.offset.x = 0;
		}

		if (rect.offset.y < 0)
		{
			rect.extent.height += rect.offset.y;
			rect.offset.y = 0;
		}

		// Check for signed overflow without relying on -fwrapv.
		if (rect.extent.width & 0x80000000u)
			rect.extent.width = 0;
		if (rect.extent.height & 0x80000000u)
			rect.extent.height = 0;
	};

	if (!degenerate && divot_image && regs.h_res > 0 && regs.v_res > 0)
	{
		VkRect2D rect = {{ regs.h_start, regs.v_start }, { uint32_t(regs.h_res), uint32_t(regs.v_res) }};
		shift_rect(rect, -int(crop_left), -int(crop_top));

		if (rect.extent.width > 0 && rect.extent.height > 0)
		{
			cmd.set_texture(0, 0, divot_image->get_view());
			cmd.set_scissor(rect);
			cmd.draw(3);
		}
	}

	// To deal with weave interlacing and other "persistence effects", we blend in previous frame's result.
	// This is somewhat arbitrary, but seems to work well enough in practice.

	if (prev_scanout_image && options.blend_previous_frame)
	{
		cmd.set_blend_enable(true);
		cmd.set_blend_factors(VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, VK_BLEND_FACTOR_DST_ALPHA);
		// Don't overwrite alpha, it's already zero.
		cmd.set_color_write_mask(0x7);
		cmd.set_specialization_constant_mask(0);
		cmd.set_texture(0, 0, prev_scanout_image->get_view());
#ifdef PARALLEL_RDP_SHADER_DIR
		cmd.set_program("rdp://fullscreen.vert", "rdp://vi_blend_fields.frag", {
				{ "DEBUG_ENABLE", debug_channel ? 1 : 0 },
		});
#else
		cmd.set_program(device->request_program(shader_bank->fullscreen, shader_bank->vi_blend_fields));
#endif

		if (degenerate)
		{
			if (regs.h_res > 0)
			{
				VkRect2D rect = {{ regs.h_start, 0 }, { uint32_t(regs.h_res), prev_scanout_image->get_height() }};
				shift_rect(rect, -int(crop_left), -int(crop_top));
				if (rect.extent.width > 0 && rect.extent.height > 0)
				{
					cmd.set_scissor(rect);
					cmd.draw(3);
				}
			}
		}
		else
		{
			// Top part.
			if (regs.h_res > 0 && regs.v_start > 0)
			{
				VkRect2D rect = {{ regs.h_start, 0 }, { uint32_t(regs.h_res), uint32_t(regs.v_start) }};
				shift_rect(rect, -int(crop_left), -int(crop_top));
				if (rect.extent.width > 0 && rect.extent.height > 0)
				{
					cmd.set_scissor(rect);
					cmd.draw(3);
				}
			}

			// Middle part, don't overwrite the 8 pixel guard band.
			if (regs.h_res_clamp > 0 && regs.v_res > 0)
			{
				VkRect2D rect = {{ regs.h_start_clamp, regs.v_start }, { uint32_t(regs.h_res_clamp), uint32_t(regs.v_res) }};
				shift_rect(rect, -int(crop_left), -int(crop_top));
				if (rect.extent.width > 0 && rect.extent.height > 0)
				{
					cmd.set_scissor(rect);
					cmd.draw(3);
				}
			}

			// Bottom part.
			if (regs.h_res > 0 && prev_scanout_image->get_height() > uint32_t(regs.v_start + regs.v_res))
			{
				VkRect2D rect = {{ regs.h_start, regs.v_start + regs.v_res },
				                 { uint32_t(regs.h_res), prev_scanout_image->get_height() - uint32_t(regs.v_start + regs.v_res) }};
				shift_rect(rect, -int(crop_left), -int(crop_top));
				if (rect.extent.width > 0 && rect.extent.height > 0)
				{
					cmd.set_scissor(rect);
					cmd.draw(3);
				}
			}
		}
	}

	cmd.end_render_pass();

	if (timestamp)
	{
		end_ts = cmd.write_timestamp(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
		device->register_time_interval("VI GPU", std::move(start_ts), std::move(end_ts), "vi-scale");
	}

	return scale_image;
}

Vulkan::ImageHandle VideoInterface::downscale_stage(Vulkan::CommandBuffer &cmd, Vulkan::Image &scale_image,
                                                    unsigned scaling_factor, unsigned downscale_steps,
                                                    const ScanoutOptions &options, bool final_pass) const
{
	Vulkan::ImageHandle downscale_image;
	const Vulkan::Image *input = &scale_image;
	Vulkan::ImageHandle holder;

	bool need_pass = scaling_factor > 1 && downscale_steps;

	// TODO: Could optimize this to happen in one pass, but ... eh.
	while (need_pass)
	{
		if (input != &scale_image)
		{
			cmd.image_barrier(*input, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			                  VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			                  VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
			                  VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_TRANSFER_READ_BIT);
		}

		unsigned width = input->get_width();
		unsigned height = input->get_height();

		Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(
				width / 2, height / 2,
				VK_FORMAT_R8G8B8A8_UNORM);

		rt_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
		rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
		rt_info.misc = Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;

		scaling_factor /= 2;
		downscale_steps--;
		need_pass = scaling_factor > 1 && downscale_steps;

		if (options.export_scanout && final_pass && !need_pass)
		{
			rt_info.misc |= Vulkan::IMAGE_MISC_EXTERNAL_MEMORY_BIT;
			rt_info.external.memory_handle_type = options.export_handle_type;
		}

		downscale_image = device->create_image(rt_info);

		if (!downscale_image)
		{
			LOGE("Failed to allocate downscale image.\n");
			return {};
		}

		cmd.image_barrier(*downscale_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
		                  0, 0, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);

		cmd.blit_image(*downscale_image, *input,
		               {}, {int(rt_info.width), int(rt_info.height), 1},
		               {}, {int(width), int(height), 1},
		               0, 0);

		input = downscale_image.get();
		holder = downscale_image;
	}

	return downscale_image;
}

Vulkan::ImageHandle VideoInterface::upscale_deinterlace(Vulkan::CommandBuffer &cmd, Vulkan::Image &scale_image,
                                                        unsigned scaling_factor, bool field_select,
                                                        const ScanoutOptions &options) const
{
	Vulkan::ImageHandle deinterlaced_image;

	// If we're running upscaled, upscaling Y further is somewhat meaningless and bandwidth intensive.
	Vulkan::ImageCreateInfo rt_info = Vulkan::ImageCreateInfo::render_target(
			scale_image.get_width(), scale_image.get_height() * (scaling_factor == 1 ? 2 : 1),
			VK_FORMAT_R8G8B8A8_UNORM);

	rt_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
	rt_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
	rt_info.misc = Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;

	if (options.export_scanout)
	{
		rt_info.misc |= Vulkan::IMAGE_MISC_EXTERNAL_MEMORY_BIT;
		rt_info.external.memory_handle_type = options.export_handle_type;
	}

	deinterlaced_image = device->create_image(rt_info);

	if (!deinterlaced_image)
	{
		LOGE("Failed to allocate deinterlace image.\n");
		return {};
	}

	Vulkan::RenderPassInfo rp;
	rp.color_attachments[0] = &deinterlaced_image->get_view();
	rp.num_color_attachments = 1;
	rp.store_attachments = 1;

	cmd.image_barrier(*deinterlaced_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
	                  0, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);

	cmd.begin_render_pass(rp);
	cmd.set_opaque_state();

	struct Push
	{
		float y_offset;
	} push = {};
	push.y_offset = (float(scaling_factor) * (field_select ? -0.25f : +0.25f)) / float(scale_image.get_height());
	cmd.push_constants(&push, 0, sizeof(push));

#ifdef PARALLEL_RDP_SHADER_DIR
	cmd.set_program("rdp://vi_deinterlace.vert", "rdp://vi_deinterlace.frag", {
		{ "DEBUG_ENABLE", debug_channel ? 1 : 0 },
	});
#else
	cmd.set_program(device->request_program(shader_bank->vi_deinterlace_vert, shader_bank->vi_deinterlace_frag));
#endif
	cmd.set_texture(0, 0, scale_image.get_view(), Vulkan::StockSampler::LinearClamp);
	cmd.draw(3);
	cmd.end_render_pass();
	return deinterlaced_image;
}

void VideoInterface::begin_vi_register_per_scanline(PerScanlineRegisterFlags flags)
{
	per_line_state.flags = flags;
	per_line_state.h_start.latched_state = vi_registers[unsigned(VIRegister::HStart)];
	per_line_state.x_scale.latched_state = vi_registers[unsigned(VIRegister::XScale)];
	per_line_state.h_start.line_state[0] = vi_registers[unsigned(VIRegister::HStart)];
	per_line_state.x_scale.line_state[0] = vi_registers[unsigned(VIRegister::XScale)];
	per_line_state.line = 0;
	per_line_state.ended = false;
}

void VideoInterface::set_vi_register_for_scanline(PerScanlineRegisterBits reg, uint32_t value)
{
	if ((per_line_state.flags & reg) == 0)
	{
		LOGW("Attempting to set VI register %u per scanline, "
		     "but was not flagged in begin_vi_register_per_scanline, ignoring.\n", reg);
		return;
	}

	switch (reg)
	{
	case PER_SCANLINE_HSTART_BIT:
		per_line_state.h_start.latched_state = value;
		break;

	case PER_SCANLINE_XSCALE_BIT:
		per_line_state.x_scale.latched_state = value;
		break;

	default:
		break;
	}
}

void VideoInterface::latch_vi_register_for_scanline(unsigned vi_line)
{
	vi_line = std::min(vi_line, VI_V_END_MAX - 1);

	if (vi_line <= per_line_state.line)
	{
		LOGW("Ignoring vi_line %u, current line is %u, not monotonically increasing, ignoring.\n",
			 vi_line, per_line_state.line);
		return;
	}

	unsigned new_counter = per_line_state.line;

	while (++new_counter < vi_line)
	{
		per_line_state.h_start.line_state[new_counter] = per_line_state.h_start.line_state[per_line_state.line];
		per_line_state.x_scale.line_state[new_counter] = per_line_state.x_scale.line_state[per_line_state.line];
	}

	per_line_state.h_start.line_state[new_counter] = per_line_state.h_start.latched_state;
	per_line_state.x_scale.line_state[new_counter] = per_line_state.x_scale.latched_state;
	per_line_state.line = new_counter;
}

void VideoInterface::clear_per_scanline_state()
{
	per_line_state.flags = 0;
	per_line_state.ended = false;
}

void VideoInterface::end_vi_register_per_scanline()
{
	if (per_line_state.flags == 0)
	{
		LOGW("Cannot end vi_register_per_scanline() with per line flags == 0, ignoring.\n");
		return;
	}

	if (per_line_state.ended)
	{
		LOGW("Already ended per line register state, ignoring.\n");
		return;
	}

	unsigned new_counter = per_line_state.line;
	while (++new_counter < VI_V_END_MAX)
	{
		per_line_state.h_start.line_state[new_counter] = per_line_state.h_start.line_state[per_line_state.line];
		per_line_state.x_scale.line_state[new_counter] = per_line_state.x_scale.line_state[per_line_state.line];
	}

	per_line_state.ended = true;
}

Vulkan::ImageHandle VideoInterface::scanout(VkImageLayout target_layout, const ScanoutOptions &options, unsigned scaling_factor_)
{
	unsigned downscale_steps = std::min(8u, options.downscale_steps);
	int scaling_factor = int(scaling_factor_);
	Vulkan::ImageHandle scanout;
	HorizontalInfoLines lines;

	auto regs = decode_vi_registers(&lines);
	clear_per_scanline_state();

	if (regs.vi_offset == 0)
	{
		prev_scanout_image.reset();
		return scanout;
	}

	if (!options.vi.serrate)
		regs.status &= ~VI_CONTROL_SERRATE_BIT;

	bool status_is_aa = (regs.status & VI_CONTROL_AA_MODE_MASK) < VI_CONTROL_AA_MODE_RESAMP_ONLY_BIT;
	bool status_is_bilinear = (regs.status & VI_CONTROL_AA_MODE_MASK) < VI_CONTROL_AA_MODE_RESAMP_REPLICATE_BIT;

	status_is_aa = status_is_aa && options.vi.aa;
	status_is_bilinear = status_is_bilinear && options.vi.scale;

	regs.status &= ~(VI_CONTROL_AA_MODE_MASK | VI_CONTROL_META_AA_BIT | VI_CONTROL_META_SCALE_BIT);
	if (status_is_aa)
		regs.status |= VI_CONTROL_META_AA_BIT;
	if (status_is_bilinear)
		regs.status |= VI_CONTROL_META_SCALE_BIT;

	if (!options.vi.gamma_dither)
		regs.status &= ~VI_CONTROL_GAMMA_DITHER_ENABLE_BIT;
	if (!options.vi.divot_filter)
		regs.status &= ~VI_CONTROL_DIVOT_ENABLE_BIT;
	if (!options.vi.dither_filter)
		regs.status &= ~VI_CONTROL_DITHER_FILTER_ENABLE_BIT;

	bool is_blank = (regs.status & VI_CONTROL_TYPE_RGBA5551_BIT) == 0;
	if (is_blank && previous_frame_blank)
	{
		frame_count++;
		prev_scanout_image.reset();
		return scanout;
	}

	if (is_blank)
		prev_scanout_image.reset();

	regs.status |= VI_CONTROL_TYPE_RGBA5551_BIT;
	previous_frame_blank = is_blank;

	bool divot = (regs.status & VI_CONTROL_DIVOT_ENABLE_BIT) != 0;

	if (regs.h_res <= 0 || regs.h_start >= VI_SCANOUT_WIDTH)
	{
		frame_count++;

		// A dirty hack to make it work for games which strobe the invalid state (but expect the image to persist),
		// and games which legitimately render invalid frames for long stretches where a black screen is expected.
		if (options.persist_frame_on_invalid_input && (frame_count - last_valid_frame_count < 4))
		{
			scanout = prev_scanout_image;

			if (scanout && prev_image_layout != target_layout)
			{
				auto cmd = device->request_command_buffer();
				cmd->image_barrier(*scanout, prev_image_layout, target_layout,
				                   layout_to_stage(prev_image_layout), 0,
				                   layout_to_stage(target_layout), layout_to_access(target_layout));
				prev_image_layout = target_layout;
				device->submit(cmd);
			}
		}
		else
			prev_scanout_image.reset();

		return scanout;
	}

	last_valid_frame_count = frame_count;

	bool degenerate = regs.h_res <= 0 || regs.v_res <= 0;

	regs.h_start *= scaling_factor;
	regs.h_start_clamp *= scaling_factor;
	regs.v_start *= scaling_factor;

	regs.h_res *= scaling_factor;
	regs.h_res_clamp *= scaling_factor;
	regs.v_res *= scaling_factor;

	regs.max_x = regs.max_x * scaling_factor + (scaling_factor - 1);
	regs.max_y = regs.max_y * scaling_factor + (scaling_factor - 1);

	for (auto &line : lines.lines)
	{
		line.h_start *= scaling_factor;
		line.h_start_clamp *= scaling_factor;
		line.h_end_clamp *= scaling_factor;
		line.x_start *= scaling_factor;
		line.y_start *= scaling_factor;
		line.y_base *= scaling_factor;
	}

	// First we copy data out of VRAM into a texture which we will then perform our post-AA on.
	// We do this on the async queue so we don't have to stall async queue on graphics work to deal with WAR hazards.
	// After the copy, we can immediately begin rendering new frames while we do post in parallel.
	Vulkan::ImageHandle vram_image;
	if (!degenerate)
		vram_image = vram_fetch_stage(regs, scaling_factor);

	auto cmd = device->request_command_buffer();

	if (debug_channel)
		cmd->begin_debug_channel(this, "VI", 32 * 1024 * 1024);

	// In the first pass, we need to read from VRAM and apply the fetch filter.
	// This is either the AA filter if coverage < 7, or the dither reconstruction filter if coverage == 7 and enabled.
	// Following that, post-AA filter, we have the divot filter.
	// In this filter, we need to find the median value of three horizontal pixels, post AA if any of them have coverage < 7.
	// Finally, we lerp the result based on x_add and y_add, and then, apply gamma/dither on top as desired.

	// AA -> divot could probably be done with compute and shared memory, but ideally this is done in fragment shaders in this implementation
	// so that we can run higher-priority compute shading workload async in the async queue.
	// We also get to take advantage of framebuffer compression FWIW.

	Vulkan::ImageHandle aa_image;
	if (!degenerate)
		aa_image = aa_fetch_stage(*cmd, *vram_image, regs, scaling_factor);

	// Divot pass
	Vulkan::ImageHandle divot_image;
	if (divot && !degenerate)
		divot_image = divot_stage(*cmd, *aa_image, regs, scaling_factor);
	else
		divot_image = std::move(aa_image);

	// Scale pass
	bool is_final_pass = !downscale_steps || scaling_factor <= 1;
	bool serrate = (regs.status & VI_CONTROL_SERRATE_BIT) != 0;

	auto scale_image = scale_stage(*cmd, divot_image.get(),
	                               regs, lines,
	                               scaling_factor, degenerate, options,
	                               is_final_pass);

	auto src_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

	if (!is_final_pass && scale_image)
	{
		cmd->image_barrier(*scale_image, src_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
		                   layout_to_stage(src_layout), layout_to_access(src_layout),
		                   VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_TRANSFER_READ_BIT);

		is_final_pass = !serrate || !options.upscale_deinterlacing;

		scale_image = downscale_stage(*cmd, *scale_image, scaling_factor, downscale_steps,
									  options, is_final_pass);

		src_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	}

	if (!is_final_pass && scale_image)
	{
		cmd->image_barrier(*scale_image, src_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
		                   layout_to_stage(src_layout), layout_to_access(src_layout),
		                   VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);

		bool field_state = regs.v_current_line == 0;
		scale_image = upscale_deinterlace(*cmd, *scale_image,
		                                  std::max(1, scaling_factor >> downscale_steps),
		                                  field_state, options);
		src_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
	}

	if (scale_image)
	{
		if (options.export_scanout)
		{
			// Foreign handle types (e.g. D3D) must use GENERAL layouts.
			if (options.export_handle_type != Vulkan::ExternalHandle::get_opaque_memory_handle_type())
				target_layout = VK_IMAGE_LAYOUT_GENERAL;

			cmd->release_image_barrier(*scale_image, src_layout,
			                           target_layout,
			                           layout_to_stage(src_layout),
			                           layout_to_access(src_layout));
		}
		else
		{
			cmd->image_barrier(*scale_image, src_layout, target_layout,
			                   layout_to_stage(src_layout), layout_to_access(src_layout),
			                   layout_to_stage(target_layout), layout_to_access(target_layout));
		}
	}

	prev_image_layout = target_layout;
	prev_scanout_image = scale_image;
	prev_image_is_external = options.export_scanout;

	if (options.persist_frame_on_invalid_input && options.export_scanout)
	{
		LOGE("persist_frame_on_invalid_input cannot be combined with export_scanout.\n");
		prev_scanout_image.reset();
	}

	device->submit(cmd);
	scanout = std::move(scale_image);
	frame_count++;
	return scanout;
}
}
