// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.

#include "engine/scene_forward_pipeline.h"
#include "engine/assets_rw_interface.h"
#include "engine/scene.h"

#include "foundation/file_rw_interface.h"
#include "foundation/format.h"
#include "foundation/log.h"
#include "foundation/projection.h"

#include "fabgen.h"

namespace hg {

static const uint64_t attribute_texture_flags = 0 | BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP;

//
void ForwardPipelineAAA::Flip(const ViewState &view_state) {
	std::swap(prv_frame_hdr_fb, next_frame_hdr_fb);
	prv_view_state = view_state;
}

//
bool IsValid(const ForwardPipelineAAA &aaa) {
	for (auto &tex : aaa.noise) {
		if (!bgfx::isValid(tex.handle)) {
			return false;
		}
	}
	if (!(bgfx::isValid(aaa.depth.handle) && bgfx::isValid(aaa.attr0.handle) && bgfx::isValid(aaa.attr1.handle))) {
		return false;
	}
	if (!bgfx::isValid(aaa.attributes_fb)) {
		return false;
	}

	if (!(IsValid(aaa.downsample) && IsValid(aaa.upsample) && IsValid(aaa.ssgi) && IsValid(aaa.ssr) && IsValid(aaa.temporal_acc) && IsValid(aaa.hiz) &&
			IsValid(aaa.taa) && IsValid(aaa.blur) && IsValid(aaa.motion_blur) && IsValid(aaa.bloom))) {
		return false;
	}
	for (int i = 0; i < 2; i++) {
		if (!(bgfx::isValid(aaa.ssgi_history[i].handle) && bgfx::isValid(aaa.ssgi_history_fb[i]) && bgfx::isValid(aaa.ssr_history[i].handle) &&
				bgfx::isValid(aaa.ssr_history_fb[i]))) {
			return false;
		}
	}

	if (aaa.ssgi_ratio != bgfx::BackbufferRatio::Equal) {
		if (!(bgfx::isValid(aaa.ssgi_output.handle) && bgfx::isValid(aaa.ssgi_output_fb))) {
			return false;
		}
	}

	if (aaa.ssr_ratio != bgfx::BackbufferRatio::Equal) {
		if (!(bgfx::isValid(aaa.ssr_output.handle) && bgfx::isValid(aaa.ssr_output_fb))) {
			return false;
		}
	}

	if (!(bgfx::isValid(aaa.work[0].handle) && bgfx::isValid(aaa.work_fb[0]))) {
		return false;
	}
	if (aaa.ssgi_ratio != aaa.ssr_ratio) {
		if (!(bgfx::isValid(aaa.work[1].handle) && bgfx::isValid(aaa.work_fb[1]))) {
			return false;
		}
	}

	return bgfx::isValid(aaa.frame_hdr.handle) && bgfx::isValid(aaa.frame_hdr_fb) && bgfx::isValid(aaa.work_frame_hdr_fb) &&
		   bgfx::isValid(aaa.prv_frame_hdr_fb) && bgfx::isValid(aaa.next_frame_hdr_fb) && bgfx::isValid(aaa.u_color) && bgfx::isValid(aaa.u_depth) &&
		   bgfx::isValid(aaa.compositing_prg);
}

static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const ReadProvider &ip, const char *path, const ForwardPipelineAAAConfig &config,
	bgfx::BackbufferRatio::Enum ssgi_ratio, bgfx::BackbufferRatio::Enum ssr_ratio) {
	ForwardPipelineAAA aaa;

	const uint64_t flags = attribute_texture_flags;

	for (size_t i = 0; i < aaa.noise.size(); ++i)
		aaa.noise[i] = LoadTexture(ir, ip, hg::format("%1/noise/LDR_RGBA_%2.png").arg(path).arg(i), 0);

	{
		// depth texture
		aaa.depth = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::D24, flags)};
		// w: linear depth, xyz: view normal
		aaa.attr0 = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		// yz: velocity
		aaa.attr1 = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		
		bgfx::TextureHandle texs[] = {aaa.depth.handle, aaa.attr0.handle, aaa.attr1.handle};
		aaa.attributes_fb = bgfx::createFrameBuffer(3, texs, true);
	}

	{
		aaa.work[0] = {flags, bgfx::createTexture2D(ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		aaa.work_fb[0] = bgfx::createFrameBuffer(1, &aaa.work[0].handle, true);
		if(ssgi_ratio != ssr_ratio) {
			aaa.work[1] = {flags, bgfx::createTexture2D(ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
			aaa.work_fb[1] = bgfx::createFrameBuffer(1, &aaa.work[1].handle, true);
		} else {
			aaa.work[1] = aaa.work[0];
			aaa.work_fb[1] = aaa.work_fb[0];
		}
	}

	{
		aaa.ssgi_ratio = ssgi_ratio;
		if (ssgi_ratio != bgfx::BackbufferRatio::Equal) {
			aaa.ssgi_output = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
			bgfx::TextureHandle texs[] = {aaa.ssgi_output.handle};
			aaa.ssgi_output_fb = bgfx::createFrameBuffer(1, texs, true);
		} else {
			aaa.ssgi_output_fb = BGFX_INVALID_HANDLE;
			aaa.ssgi_output = {};
		}
		aaa.ssgi = CreateSSGIFromAssets(path);

		aaa.ssgi_history[0] = {flags, bgfx::createTexture2D(aaa.ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		aaa.ssgi_history[1] = {flags, bgfx::createTexture2D(aaa.ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		
		aaa.ssgi_history_fb[0] = bgfx::createFrameBuffer(1, &aaa.ssgi_history[0].handle, true);
		aaa.ssgi_history_fb[1] = bgfx::createFrameBuffer(1, &aaa.ssgi_history[1].handle, true);
	}

	{
		aaa.ssr_ratio = ssr_ratio;
		if (ssr_ratio != bgfx::BackbufferRatio::Equal) {
			aaa.ssr_output = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
			bgfx::TextureHandle texs[] = {aaa.ssr_output.handle};
			aaa.ssr_output_fb = bgfx::createFrameBuffer(1, texs, true);
		} else {
			aaa.ssr_output_fb = BGFX_INVALID_HANDLE;
			aaa.ssr_output = {};
		}
		aaa.ssr = CreateSSRFromAssets(path);

		aaa.ssr_history[0] = {flags, bgfx::createTexture2D(aaa.ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		
		aaa.ssr_history[1] = {flags, bgfx::createTexture2D(aaa.ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)};
		
		aaa.ssr_history_fb[0] = bgfx::createFrameBuffer(1, &aaa.ssr_history[0].handle, true);
		aaa.ssr_history_fb[1] = bgfx::createFrameBuffer(1, &aaa.ssr_history[1].handle, true);
	}

	{ aaa.blur = CreateAAABlurFromAssets(path); }

	{ aaa.hiz = CreateHiZFromAssets(path, hg::Min(ssgi_ratio, ssr_ratio)); }

	{ aaa.downsample = CreateDownsampleFromAssets(path); }

	{ aaa.upsample = CreateUpsampleFromAssets(path); }

	{ aaa.temporal_acc = CreateTemporalAccumulationFromAssets(path); }

	{ aaa.motion_blur = CreateMotionBlurFromAssets(path); }

	{
		// RGBA16F
		aaa.frame_hdr = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)};

		bgfx::TextureHandle texs[] = {aaa.depth.handle, aaa.frame_hdr.handle};
		aaa.frame_hdr_fb = bgfx::createFrameBuffer(2, texs, false);
		
		aaa.work_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags);
		
		aaa.prv_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags); // (reprojected) input to SSR/SSGI
		aaa.next_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags); // current frame with SSR/SSGI
	}

	{
		aaa.compositing_prg = hg::LoadProgram(ir, ip, hg::format("%1/shader/compositing").arg(path).c_str());
		aaa.u_color = bgfx::createUniform("u_color", bgfx::UniformType::Sampler);
		aaa.u_depth = bgfx::createUniform("u_depth", bgfx::UniformType::Sampler);
	}

	aaa.taa = CreateTAAFromAssets(path);
	aaa.bloom = CreateBloomFromAssets(hg::format("%1/shader").arg(path), bgfx::BackbufferRatio::Equal);

	if (!IsValid(aaa)) {
		DestroyForwardPipelineAAA(aaa);
		return aaa;
	}

	bgfx::setName(aaa.depth.handle, "aaa.depth");
	bgfx::setName(aaa.attr0.handle, "aaa.attr0");
	bgfx::setName(aaa.attr1.handle, "aaa.attr1");
	bgfx::setName(aaa.attributes_fb, "Attributes FB");
	bgfx::setName(aaa.work[0].handle, "aaa.work[0]");
	bgfx::setName(aaa.work_fb[0], "Work FB #0");
	bgfx::setName(aaa.ssgi_history[0].handle, "aaa.ssgi_history_0");
	bgfx::setName(aaa.ssgi_history[1].handle, "aaa.ssgi_history_1");
	bgfx::setName(aaa.ssr_history[0].handle, "aaa.ssr_history_0");
	bgfx::setName(aaa.ssr_history[1].handle, "aaa.ssr_history_1");
	bgfx::setName(aaa.frame_hdr_fb, "Frame HDR FB");
	bgfx::setName(aaa.work_frame_hdr_fb, "Work HDR frame FB");
	bgfx::setName(aaa.prv_frame_hdr_fb, "Previous HDR frame FB");
	bgfx::setName(aaa.next_frame_hdr_fb, "Next HDR frame FB");
	if (ssgi_ratio != bgfx::BackbufferRatio::Equal) {
		bgfx::setName(aaa.ssgi_output.handle, "aaa.ssgi_output");		
	}
	if (ssr_ratio != bgfx::BackbufferRatio::Equal) {
		bgfx::setName(aaa.ssr_output.handle, "aaa.ssr_output");
	}
	if (ssgi_ratio != ssr_ratio) {
		bgfx::setName(aaa.work[1].handle, "aaa.work[1]");
		bgfx::setName(aaa.work_fb[1], "Work FB #1");
	}
	return aaa;
}

ForwardPipelineAAA CreateForwardPipelineAAAFromFile(
	const char *path, const ForwardPipelineAAAConfig &config, bgfx::BackbufferRatio::Enum ssgi_ratio, bgfx::BackbufferRatio::Enum ssr_ratio) {
	return _CreateForwardPipelineAAA(g_file_reader, g_file_read_provider, path, config, ssgi_ratio, ssr_ratio);
}

ForwardPipelineAAA CreateForwardPipelineAAAFromAssets(
	const char *path, const ForwardPipelineAAAConfig &config, bgfx::BackbufferRatio::Enum ssgi_ratio, bgfx::BackbufferRatio::Enum ssr_ratio) {
	return _CreateForwardPipelineAAA(g_assets_reader, g_assets_read_provider, path, config, ssgi_ratio, ssr_ratio);
}

void DestroyForwardPipelineAAA(ForwardPipelineAAA &aaa) {
	for (size_t i = 0; i < aaa.noise.size(); ++i)
		Destroy(aaa.noise[i]);

	//
	if(aaa.work_fb[1].idx != aaa.work_fb[0].idx) {
		bgfx_Destroy(aaa.work_fb[1]);
	}
	aaa.work_fb[1] = BGFX_INVALID_HANDLE;
	bgfx_Destroy(aaa.work_fb[0])

	//
	bgfx_Destroy(aaa.attributes_fb); // destroys depth, attr0, attr1

	//
	bgfx_Destroy(aaa.ssgi_output_fb);
	DestroySSGI(aaa.ssgi);
	bgfx_Destroy(aaa.ssr_output_fb);
	DestroySSR(aaa.ssr);

	for (int i = 0; i < 2; i++) {
		bgfx_Destroy(aaa.ssgi_history_fb[i]);
		bgfx_Destroy(aaa.ssr_history_fb[i]);
	}

	//
	DestroyAAABlur(aaa.blur);

	//
#if 0
	DestroySAO(aaa.sao);
#endif
	DestroyMotionBlur(aaa.motion_blur);

	//
	Destroy(aaa.frame_hdr);

	bgfx_Destroy(aaa.frame_hdr_fb);
	bgfx_Destroy(aaa.work_frame_hdr_fb);
	bgfx_Destroy(aaa.prv_frame_hdr_fb);
	bgfx_Destroy(aaa.next_frame_hdr_fb);

	//
	bgfx_Destroy(aaa.compositing_prg);
	bgfx_Destroy(aaa.u_color);
	bgfx_Destroy(aaa.u_depth);

	//
	DestroyTAA(aaa.taa);
	DestroyBloom(aaa.bloom);
	DestroyHiZ(aaa.hiz);

	//
	DestroyDownsample(aaa.downsample);

	DestroyUpsample(aaa.upsample);

	DestroyTemporalAccumulation(aaa.temporal_acc);
}

//
void GetSceneForwardPipelineLights(const Scene &scene, std::vector<ForwardPipelineLight> &out_lights) {
	out_lights.clear();

	const auto lights = scene.GetLights();

	out_lights.reserve(lights.size());

	for (const auto &light : lights) {
		if (!light.IsEnabled())
			continue;

		const auto trs = light.GetTransform();
		const auto lgt = light.GetLight();

		ForwardPipelineLight lgt_;

		lgt_.world = scene.GetTransformWorldMatrix(trs.ref.idx);
		lgt_.diffuse = lgt.GetDiffuseColor() * lgt.GetDiffuseIntensity();
		lgt_.specular = lgt.GetSpecularColor() * lgt.GetSpecularIntensity();

		const auto light_type = lgt.GetType();

		if (light_type == LT_Linear) {
			lgt_.type = FPLT_Linear;
			lgt_.pssm_split = lgt.GetPSSMSplit();
			lgt_.radius = 0.f;
			lgt_.inner_angle = 0.f;
			lgt_.outer_angle = 0.f;
		} else if (light_type == LT_Spot) {
			lgt_.type = FPLT_Spot;
			lgt_.pssm_split = hg::Vec4::Zero;
			lgt_.radius = lgt.GetRadius();
			lgt_.inner_angle = lgt.GetInnerAngle();
			lgt_.outer_angle = lgt.GetOuterAngle();
		} else { // fall back to point
			lgt_.type = FPLT_Point;
			lgt_.pssm_split = hg::Vec4::Zero;
			lgt_.radius = lgt.GetRadius();
			lgt_.inner_angle = 0.f;
			lgt_.outer_angle = 0.f;
		}

		const auto shadow_type = lgt.GetShadowType();

		if (shadow_type == LST_Map)
			lgt_.shadow_type = FPST_Map;
		else
			lgt_.shadow_type = FPST_None;

		lgt_.priority = lgt.GetPriority();
		lgt_.shadow_bias = lgt.GetShadowBias();

		out_lights.push_back(lgt_);
	}
}

//
std::vector<uint32_t> ComputeModelDisplayListSortKeys(
	const Scene &scene, const ViewState &view_state, const std::vector<ModelDisplayList> &dls, const PipelineResources &res) {
	std::vector<uint32_t> sort_keys;
	sort_keys.reserve(dls.size());

	const auto &mtxs = scene.GetTransformWorldMatrices();

	for (const auto &dl : dls) {
		const auto &mdl_mtx = mtxs[dl.mtx_idx]; // model matrix
		const auto &mdl_view_mtx = view_state.view * mdl_mtx; // TODO cache based on dl.mtx_idx
		const auto &mdl = res.models.Get_unsafe_(dl.mdl_idx);

#if 0
		const auto &dl_center = GetCenter(mdl.bounds[dl.lst_idx]); // display list bounding volume center
		const auto &dl_center_in_view = dl_center * mdl_view_mtx;
		sort_keys.push_back(uint32_t(dl_center_in_view.z * 1000.f)); // sort to mm precision
#else
		const auto &bound = mdl.bounds[dl.lst_idx];

		const Vec3 vtx[8] = {
			{bound.mn.x, bound.mn.y, bound.mn.z},
			{bound.mx.x, bound.mn.y, bound.mn.z},
			{bound.mx.x, bound.mx.y, bound.mn.z},
			{bound.mn.x, bound.mx.y, bound.mn.z},
			{bound.mn.x, bound.mn.y, bound.mx.z},
			{bound.mx.x, bound.mn.y, bound.mx.z},
			{bound.mx.x, bound.mx.y, bound.mx.z},
			{bound.mn.x, bound.mx.y, bound.mx.z},
		};

		float closest = std::numeric_limits<float>::max();

		for (auto &v : vtx) {
			const auto &in_view = v * mdl_view_mtx;
			if (in_view.z < closest)
				closest = in_view.z;
		}

		if (closest < 0.f)
			closest = 0.f;

		sort_keys.push_back(ComputeSortKey(closest)); // sort to mm precision
#endif
	}

	return sort_keys;
}

std::vector<uint32_t> ComputeSkinnedModelDisplayListSortKeys(
	const Scene &scene, const ViewState &view_state, const std::vector<SkinnedModelDisplayList> &dls, const PipelineResources &res) {
	std::vector<uint32_t> sort_keys;
	sort_keys.reserve(dls.size());

	const auto &mtxs = scene.GetTransformWorldMatrices();

	for (const auto &dl : dls) {
		// TODO
		sort_keys.push_back(0);
	}

	return sort_keys;
}

static int ComputeForwardPipelineConfigurationIdx(ForwardPipelineStage pipeline_stage, const ForwardPipelineLights &lights) {
	if (pipeline_stage == FPS_AttributeBuffers)
		return 0;

	int light_config_idx = 0;

	if (lights.lights[0].shadow_type == FPST_Map)
		light_config_idx |= 0b01;
	if (lights.lights[1].shadow_type == FPST_Map)
		light_config_idx |= 0b10;

	if (pipeline_stage == FPS_Basic)
		return light_config_idx + 1;

	if (pipeline_stage == FPS_Advanced)
		return 4 + light_config_idx + 1;

	return -1;
}

//
void PrepareSceneForwardPipelineCommonRenderData(bgfx::ViewId &view_id, const Scene &scene, SceneForwardPipelineRenderData &render_data,
	const ForwardPipeline &pipeline, const PipelineResources &resources, SceneForwardPipelinePassViewId &views, const char *debug_name) {
	scene.GetModelDisplayLists(
		render_data.all_opaque, render_data.all_transparent, render_data.all_opaque_skinned, render_data.all_transparent_skinned, resources);

	std::vector<ForwardPipelineLight> lights;
	GetSceneForwardPipelineLights(scene, lights);
	render_data.pipe_lights = PrepareForwardPipelineLights(lights);

	ForwardPipelineShadowPassViewId sp_views;
	GenerateSpotShadowMapForForwardPipeline(view_id, render_data.all_opaque, render_data.all_opaque_skinned, scene.GetTransformWorldMatrices(),
		render_data.pipe_lights, pipeline, resources, sp_views, render_data.shadow_data, debug_name);

	views[FPSP_Slot1Spot] = sp_views[FPSP_Slot1Spot];
}

//
void PrepareSceneForwardPipelineViewDependentRenderData(bgfx::ViewId &view_id, const ViewState &view_state, const Scene &scene,
	SceneForwardPipelineRenderData &render_data, const ForwardPipeline &pipeline, const PipelineResources &resources, SceneForwardPipelinePassViewId &views,
	const char *debug_name) {

	ForwardPipelineShadowPassViewId sp_views;
	GenerateLinearShadowMapForForwardPipeline(view_id, view_state, render_data.all_opaque, render_data.all_opaque_skinned, scene.GetTransformWorldMatrices(),
		render_data.pipe_lights, pipeline, resources, sp_views, render_data.shadow_data, debug_name);

	views[SFPP_Slot0LinearSplit0] = sp_views[FPSP_Slot0LinearSplit0];
	views[SFPP_Slot0LinearSplit1] = sp_views[FPSP_Slot0LinearSplit1];
	views[SFPP_Slot0LinearSplit2] = sp_views[FPSP_Slot0LinearSplit2];
	views[SFPP_Slot0LinearSplit3] = sp_views[FPSP_Slot0LinearSplit3];

	render_data.view_opaque = render_data.all_opaque;
	CullModelDisplayLists(view_state.frustum, render_data.view_opaque, scene.GetTransformWorldMatrices(), resources);
	render_data.view_transparent = render_data.all_transparent;
	CullModelDisplayLists(view_state.frustum, render_data.view_transparent, scene.GetTransformWorldMatrices(), resources);

	// FIXME cull skinned models !!!
	render_data.view_opaque_skinned = render_data.all_opaque_skinned;
	render_data.view_transparent_skinned = render_data.all_transparent_skinned;
		
	render_data.fog = GetSceneForwardPipelineFog(scene);
}

//
ForwardPipelineFog GetSceneForwardPipelineFog(const Scene &scene) {
	return {scene.environment.fog_near, scene.environment.fog_far, scene.environment.fog_color};
}

//
static iRect ScreenSpaceRect(const iRect &rect, bgfx::BackbufferRatio::Enum ratio) {
	if (ratio == bgfx::BackbufferRatio::Half)
		return rect / 2;
	if (ratio == bgfx::BackbufferRatio::Quarter)
		return rect / 4;
	if (ratio == bgfx::BackbufferRatio::Eighth)
		return rect / 8;
	if (ratio == bgfx::BackbufferRatio::Sixteenth)
		return rect / 16;
	if (ratio == bgfx::BackbufferRatio::Double)
		return rect * 2;
	return rect;
}

void SubmitSceneToForwardPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, const ViewState &view_state, ForwardPipeline &pipeline,
	const SceneForwardPipelineRenderData &render_data, const PipelineResources &resources, SceneForwardPipelinePassViewId &views, const ForwardPipelineAAA &aaa,
	const ForwardPipelineAAAConfig &aaa_config, int frame, bgfx::FrameBufferHandle fb, const char *debug_name) {
	__ASSERT__(bgfx::getCaps()->supported & BGFX_CAPS_TEXTURE_BLIT);

	// reset pass views
	std::fill(std::begin(views), std::end(views), 65535);

	// update pipeline
	UpdateForwardPipeline(pipeline, render_data.shadow_data, scene.environment.ambient, render_data.pipe_lights, render_data.fog);
	UpdateForwardPipelineAAA(pipeline, rect, view_state.view, view_state.proj, aaa.prv_view_state.view, aaa.prv_view_state.proj, TAAProjectionJitter8(frame),
		aaa.ssgi_ratio, aaa.ssr_ratio, aaa_config.temporal_aa_weight, aaa_config.motion_blur, aaa_config.exposure, aaa_config.gamma, aaa_config.sample_count,
		aaa_config.max_distance); // [todo] ssgi_ratio/ssr_ratio

	// TAA jittered projection matrix
	const auto jitter = TAAProjectionJitter8(frame) / Vec2(float(GetWidth(rect)), float(GetHeight(rect)));
	const auto proj_jittered = to_bgfx(TranslationMat4({jitter.x, jitter.y, 0}) * view_state.proj);

	// fill attribute buffers (linear depth/normal/velocity)
	{
		bgfx::touch(view_id);
		bgfx::setViewName(view_id, hg::format("Attribute buffers: %1").arg(debug_name).c_str());
		bgfx::setViewRect(view_id, rect.sx, rect.sy, GetWidth(rect), GetHeight(rect));
		bgfx::setViewFrameBuffer(view_id, aaa.attributes_fb);

		bgfx::setViewClear(view_id, BGFX_CLEAR_DEPTH | BGFX_CLEAR_COLOR, 0, 1.f, 0);

		bgfx::setViewMode(bgfx::ViewMode::Default);
		bgfx::setViewTransform(view_id, to_bgfx(view_state.view).data(), proj_jittered.data());

		const int pipeline_config_idx = ComputeForwardPipelineConfigurationIdx(FPS_AttributeBuffers, render_data.pipe_lights);

		DrawModelDisplayLists(view_id, render_data.view_opaque, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), scene.GetPreviousTransformWorldMatrices(), resources);
		DrawSkinnedModelDisplayLists(view_id, render_data.view_opaque_skinned, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), scene.GetPreviousTransformWorldMatrices(), resources);

		views[SFPP_DepthPrepass] = view_id++;
	}
	
	// animated blue noise texture
	const Texture &noise = aaa.noise[frame % aaa.noise.size()];

	UpdateForwardPipelineNoise(pipeline, noise);

	// Downsample
	if ((aaa.ssgi_ratio != bgfx::BackbufferRatio::Equal) || (aaa.ssr_ratio != bgfx::BackbufferRatio::Equal)) {
		ComputeDownsample(view_id, ScreenSpaceRect(rect, bgfx::BackbufferRatio::Half), {attribute_texture_flags, bgfx::getTexture(aaa.prv_frame_hdr_fb)},
			aaa.attr0, aaa.depth, aaa.downsample);
	}

	// HiZ
	{
		if ((aaa.ssgi_ratio != bgfx::BackbufferRatio::Equal) && (aaa.ssr_ratio != bgfx::BackbufferRatio::Equal)) {
			ComputeHiZ(view_id, ScreenSpaceRect(rect, bgfx::BackbufferRatio::Half), view_state.proj, aaa.downsample.attr0, aaa.hiz);
		} else {
			ComputeHiZ(view_id, rect, view_state.proj, aaa.attr0, aaa.hiz);
		}
	}

	int current = frame & 1;
	int previous = current ^ 1;

	hg::Texture ssgi_output;
	hg::Texture ssr_output;

	// SSGI
	{
		auto &probe_map = resources.textures.Get(scene.environment.irradiance_map);
		if (aaa.ssgi_ratio != bgfx::BackbufferRatio::Equal) {
			ComputeSSGI(view_id, ScreenSpaceRect(rect, aaa.ssgi_ratio), aaa.ssgi_ratio, aaa.downsample.color, aaa.downsample.attr0, aaa.attr1, // [todo] downsample attr1 ?!
				probe_map, noise, aaa.hiz, aaa.work_fb[0], aaa.ssgi);

			ComputeTemporalAccumulation(view_id, ScreenSpaceRect(rect, aaa.ssgi_ratio), aaa.work[0], aaa.ssgi_history[previous], aaa.attr1,
				aaa.ssgi_history_fb[current], aaa.temporal_acc);

			ComputeAAABlur(view_id, ScreenSpaceRect(rect, aaa.ssgi_ratio), aaa.downsample.attr0, aaa.ssgi_history_fb[current], aaa.work_fb[0], aaa.blur);

			ComputeUpsample(view_id, rect, aaa.ssgi_history[current], aaa.downsample.attr0, aaa.attr0, aaa.ssgi_output_fb, aaa.upsample);
			ssgi_output = aaa.ssgi_output;
		} else {
			ComputeSSGI(view_id, rect, aaa.ssgi_ratio, {attribute_texture_flags, bgfx::getTexture(aaa.prv_frame_hdr_fb)}, aaa.attr0, aaa.attr1, probe_map, noise, aaa.hiz, aaa.work_fb[0], aaa.ssgi);

			ComputeTemporalAccumulation(view_id, rect, aaa.work[0], aaa.ssgi_history[previous], aaa.attr1,
				aaa.ssgi_history_fb[current], aaa.temporal_acc);

			ComputeAAABlur(view_id, ScreenSpaceRect(rect, aaa.ssgi_ratio), aaa.attr0, aaa.ssgi_history_fb[current], aaa.work_fb[0], aaa.blur);

			ssgi_output = aaa.ssgi_history[current];
		}
	}

	// SSR
	{
		auto &probe_map = resources.textures.Get(scene.environment.radiance_map);
		if (aaa.ssr_ratio != bgfx::BackbufferRatio::Equal) {
			ComputeSSR(view_id, ScreenSpaceRect(rect, aaa.ssr_ratio), aaa.ssr_ratio, aaa.downsample.color, aaa.downsample.attr0, aaa.attr1, // [todo] downsample attr1 ?!
				probe_map, noise, aaa.hiz, aaa.work_fb[1], aaa.ssr);

			ComputeTemporalAccumulation(
				view_id, ScreenSpaceRect(rect, aaa.ssr_ratio), aaa.work[1], aaa.ssr_history[previous], aaa.attr1, aaa.ssr_history_fb[current], aaa.temporal_acc);

			ComputeUpsample(view_id, rect, aaa.ssr_history[current], aaa.downsample.attr0, aaa.attr0, aaa.ssr_output_fb, aaa.upsample);
			ssr_output = aaa.ssr_output;
		} else {
			ComputeSSR(view_id, rect, aaa.ssr_ratio, {attribute_texture_flags, bgfx::getTexture(aaa.prv_frame_hdr_fb)}, aaa.attr0, aaa.attr1, probe_map, noise, aaa.hiz,
				aaa.work_fb[1], aaa.ssr);
			ComputeTemporalAccumulation(view_id, rect, aaa.work[1], aaa.ssr_history[previous], aaa.attr1, aaa.ssr_history_fb[current], aaa.temporal_acc);
			ssr_output = aaa.ssr_history[current];
		}
	}

#if 0
	// SAO [todo] move to forward pipeline
	{
		ComputeSAO(view_id, ScreenSpaceRect(rect, aaa.ssgi_ratio), aaa.attr0, aaa.attr1, noise, aaa.sao_output_fb, aaa.sao, view_state.proj, aaa.sao_bias, // [todo] aaa.ssgi_ratio : add ratio to SAO struct
			aaa.sao_radius,
		aaa.sao_sample_count, aaa.sao_sharpness);
		UpdateForwardPipelineAO(pipeline, aaa.sao_output);
	}
#endif

	// setup environment for AAA pipeline
	{
		auto &brdf_map = resources.textures.Get(scene.environment.brdf_map);
		UpdateForwardPipelinePBRProbe(pipeline, ssgi_output, ssr_output, brdf_map);
	}

	// opaque pass
	{
		bgfx::touch(view_id);
		bgfx::setViewName(view_id, hg::format("Opaque pass: %1").arg(debug_name).c_str());
		bgfx::setViewRect(view_id, rect.sx, rect.sy, GetWidth(rect), GetHeight(rect));
		bgfx::setViewFrameBuffer(view_id, aaa.frame_hdr_fb);

		if (scene.canvas.clear_z || scene.canvas.clear_color) {
			const uint16_t flags = (scene.canvas.clear_z ? BGFX_CLEAR_DEPTH : 0) | (scene.canvas.clear_color ? BGFX_CLEAR_COLOR : 0);
			const uint32_t color = ColorToABGR32(scene.canvas.color);
			bgfx::setViewClear(view_id, flags, color, 1.f, 0);
		} else {
			bgfx::setViewClear(view_id, BGFX_CLEAR_NONE, 0, 1.f, UINT8_MAX);
		}

		bgfx::setViewMode(bgfx::ViewMode::Default);
		bgfx::setViewTransform(view_id, to_bgfx(view_state.view).data(), proj_jittered.data());

		const int pipeline_config_idx = ComputeForwardPipelineConfigurationIdx(FPS_Advanced, render_data.pipe_lights);

		DrawModelDisplayLists(view_id, render_data.view_opaque, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), resources);
		DrawSkinnedModelDisplayLists(view_id, render_data.view_opaque_skinned, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), resources);

		views[SFPP_Opaque] = view_id++;
	}

	// setup environment for non AAA pipeline
	{
		auto &irradiance_map = resources.textures.Get(scene.environment.irradiance_map);
		auto &radiance_map = resources.textures.Get(scene.environment.radiance_map);
		auto &brdf_map = resources.textures.Get(scene.environment.brdf_map);
		UpdateForwardPipelinePBRProbe(pipeline, irradiance_map, radiance_map, brdf_map);
	}

	// transparent pass
	{
		bgfx::touch(view_id);
		bgfx::setViewName(view_id, hg::format("Transparent pass: %1").arg(debug_name).c_str());
		bgfx::setViewRect(view_id, rect.sx, rect.sy, GetWidth(rect), GetHeight(rect));
		bgfx::setViewFrameBuffer(view_id, aaa.frame_hdr_fb);

		bgfx::setViewClear(view_id, BGFX_CLEAR_NONE, 0, 1.f, UINT8_MAX);
		bgfx::setViewMode(view_id, bgfx::ViewMode::DepthDescending);
		bgfx::setViewTransform(view_id, to_bgfx(view_state.view).data(), proj_jittered.data());

		const int pipeline_config_idx = ComputeForwardPipelineConfigurationIdx(FPS_Basic, render_data.pipe_lights);

		const auto view_transparent_sort_keys = ComputeModelDisplayListSortKeys(scene, view_state, render_data.view_transparent, resources);
		DrawModelDisplayLists(view_id, render_data.view_transparent, view_transparent_sort_keys, pipeline_config_idx, pipeline.uniform_values,
			pipeline.uniform_textures, scene.GetTransformWorldMatrices(), resources);

		const auto view_transparent_skinned_sort_keys =
			ComputeSkinnedModelDisplayListSortKeys(scene, view_state, render_data.view_transparent_skinned, resources);
		DrawSkinnedModelDisplayLists(view_id, render_data.view_transparent_skinned, view_transparent_skinned_sort_keys, pipeline_config_idx,
			pipeline.uniform_values, pipeline.uniform_textures, scene.GetTransformWorldMatrices(), resources);

		views[SFPP_Transparent] = view_id++;
	}

	// TAA
	ApplyTAA(
		view_id, rect, aaa.frame_hdr, {attribute_texture_flags, bgfx::getTexture(aaa.prv_frame_hdr_fb)}, aaa.attr0, aaa.attr1, aaa.next_frame_hdr_fb, aaa.taa);

	// motion blur
	ApplyMotionBlur(
		view_id, rect, {attribute_texture_flags, bgfx::getTexture(aaa.next_frame_hdr_fb)}, aaa.attr0, aaa.attr1, noise, aaa.work_frame_hdr_fb, aaa.motion_blur);

	// bloom
	ApplyBloom(view_id, rect, {attribute_texture_flags, bgfx::getTexture(aaa.work_frame_hdr_fb)}, aaa.frame_hdr_fb, aaa.bloom, aaa_config.bloom_threshold,
		aaa_config.bloom_bias, aaa_config.bloom_intensity);

	// final compositing for presentation (exposure/gamma correction)
	{
		if (bgfx::isValid(fb))
			bgfx::setViewName(view_id, "Final compositing to target framebuffer");
		else
			bgfx::setViewName(view_id, "Final compositing to back buffer");

		auto stats = bgfx::getStats();
		bgfx::setViewRect(view_id, 0, 0, stats->width, stats->height);
		bgfx::setViewFrameBuffer(view_id, fb);

		bgfx::setViewClear(view_id, BGFX_CLEAR_NONE, 0, 1.f, UINT8_MAX);
		bgfx::setViewTransform(view_id, nullptr, nullptr);

		if (aaa_config.debug_buffer == FPAAADB_None)
			bgfx::setTexture(0, aaa.u_color, aaa.frame_hdr.handle, uint32_t(attribute_texture_flags));
		else if (aaa_config.debug_buffer == FPAAADB_SSGI)
			bgfx::setTexture(0, aaa.u_color, ssgi_output.handle, uint32_t(attribute_texture_flags));
		else if (aaa_config.debug_buffer == FPAAADB_SSR)
			bgfx::setTexture(0, aaa.u_color, ssr_output.handle, uint32_t(attribute_texture_flags));
		bgfx::setTexture(1, aaa.u_depth, aaa.depth.handle, uint32_t(aaa.depth.flags));

		bgfx::VertexLayout decl;
		decl.begin().add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float).add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float).end();

		const float k_y = bgfx::getCaps()->originBottomLeft ? 0.f : 1.f;

		Vertices vtx(decl, 4);
		vtx.Begin(0).SetPos({-1, -1, 0}).SetTexCoord0({0, k_y}).End();
		vtx.Begin(1).SetPos({1, -1, 0}).SetTexCoord0({1, k_y}).End();
		vtx.Begin(2).SetPos({1, 1, 0}).SetTexCoord0({1, 1.f - k_y}).End();
		vtx.Begin(3).SetPos({-1, 1, 0}).SetTexCoord0({0, 1.f - k_y}).End();
		DrawTriangles(view_id, {0, 1, 2, 0, 2, 3}, vtx, aaa.compositing_prg, {}, {}, ComputeRenderState(BM_Opaque, DT_Always));

		++view_id;
	}
}

// basic rendering path
void SubmitSceneToForwardPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, const ViewState &view_state, ForwardPipeline &pipeline,
	const SceneForwardPipelineRenderData &render_data, const PipelineResources &resources, SceneForwardPipelinePassViewId &views, bgfx::FrameBufferHandle fb,
	const char *debug_name) {
	std::fill(std::begin(views), std::end(views), 65535);

	// update pipeline
	UpdateForwardPipeline(pipeline, render_data.shadow_data, scene.environment.ambient, render_data.pipe_lights, render_data.fog);
	//	UpdateForwardPipelineAAA(pipeline, rect, view_state.view, view_state.proj, view_state.view, view_state.proj, {});

	{
		auto &irradiance_map = resources.textures.Get(scene.environment.irradiance_map);
		auto &radiance_map = resources.textures.Get(scene.environment.radiance_map);
		auto &brdf_map = resources.textures.Get(scene.environment.brdf_map);
		UpdateForwardPipelinePBRProbe(pipeline, irradiance_map, radiance_map, brdf_map);
	}

	const int pipeline_config_idx = ComputeForwardPipelineConfigurationIdx(FPS_Basic, render_data.pipe_lights);

	// opaque pass
	{
		bgfx::touch(view_id);
		bgfx::setViewName(view_id, hg::format("Opaque pass: %1").arg(debug_name).c_str());
		bgfx::setViewRect(view_id, rect.sx, rect.sy, GetWidth(rect), GetHeight(rect));
		bgfx::setViewFrameBuffer(view_id, fb);

		if (scene.canvas.clear_z || scene.canvas.clear_color) {
			const uint16_t flags = (scene.canvas.clear_z ? BGFX_CLEAR_DEPTH : 0) | (scene.canvas.clear_color ? BGFX_CLEAR_COLOR : 0);
			const uint32_t color = ColorToABGR32(scene.canvas.color);
			bgfx::setViewClear(view_id, flags, color, 1.f, 0);
		} else {
			bgfx::setViewClear(view_id, BGFX_CLEAR_NONE, 0, 1.f, UINT8_MAX);
		}

		bgfx::setViewMode(bgfx::ViewMode::Default);
		bgfx::setViewTransform(view_id, to_bgfx(view_state.view).data(), to_bgfx(view_state.proj).data());

		DrawModelDisplayLists(view_id, render_data.view_opaque, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), resources);
		DrawSkinnedModelDisplayLists(view_id, render_data.view_opaque_skinned, pipeline_config_idx, pipeline.uniform_values, pipeline.uniform_textures,
			scene.GetTransformWorldMatrices(), resources);

		views[SFPP_Opaque] = view_id++;
	}

	// transparent pass
	{
		bgfx::touch(view_id);
		bgfx::setViewName(view_id, hg::format("Transparent pass: %1").arg(debug_name).c_str());
		bgfx::setViewRect(view_id, rect.sx, rect.sy, GetWidth(rect), GetHeight(rect));
		bgfx::setViewFrameBuffer(view_id, fb);

		bgfx::setViewClear(view_id, BGFX_CLEAR_NONE, 0, 1.f, UINT8_MAX);
		bgfx::setViewMode(view_id, bgfx::ViewMode::DepthDescending);
		bgfx::setViewTransform(view_id, to_bgfx(view_state.view).data(), to_bgfx(view_state.proj).data());

		const auto view_transparent_sort_keys = ComputeModelDisplayListSortKeys(scene, view_state, render_data.view_transparent, resources);
		DrawModelDisplayLists(view_id, render_data.view_transparent, view_transparent_sort_keys, pipeline_config_idx, pipeline.uniform_values,
			pipeline.uniform_textures, scene.GetTransformWorldMatrices(), resources);

		const auto view_transparent_skinned_sort_keys =
			ComputeSkinnedModelDisplayListSortKeys(scene, view_state, render_data.view_transparent_skinned, resources);
		DrawSkinnedModelDisplayLists(view_id, render_data.view_transparent_skinned, view_transparent_skinned_sort_keys, pipeline_config_idx,
			pipeline.uniform_values, pipeline.uniform_textures, scene.GetTransformWorldMatrices(), resources);

		views[SFPP_Transparent] = view_id++;
	}
}

//
void SubmitSceneToPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, const ViewState &view_state, ForwardPipeline &pipeline,
	const PipelineResources &resources, SceneForwardPipelinePassViewId &views, bgfx::FrameBufferHandle fb, const char *debug_name) {
	SceneForwardPipelineRenderData render_data;
	PrepareSceneForwardPipelineCommonRenderData(view_id, scene, render_data, pipeline, resources, views, debug_name);
	PrepareSceneForwardPipelineViewDependentRenderData(view_id, view_state, scene, render_data, pipeline, resources, views, debug_name);
	SubmitSceneToForwardPipeline(view_id, scene, rect, view_state, pipeline, render_data, resources, views, fb, debug_name);
}

void SubmitSceneToPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, bool fov_axis_is_horizontal, ForwardPipeline &pipeline,
	const PipelineResources &resources, SceneForwardPipelinePassViewId &views, bgfx::FrameBufferHandle fb, const char *debug_name) {
	const auto w = float(GetWidth(rect)), h = float(GetHeight(rect));
	const auto ar = fov_axis_is_horizontal ? ComputeAspectRatioX(w, h) : ComputeAspectRatioY(w, h);

	const auto view_state = scene.ComputeCurrentCameraViewState(ar);
	SubmitSceneToPipeline(view_id, scene, rect, view_state, pipeline, resources, views, fb, debug_name);
}

//
void SubmitSceneToPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, const ViewState &view_state, ForwardPipeline &pipeline,
	const PipelineResources &resources, SceneForwardPipelinePassViewId &views, ForwardPipelineAAA &aaa, const ForwardPipelineAAAConfig &aaa_config, int frame,
	bgfx::FrameBufferHandle fb, const char *debug_name) {
	SceneForwardPipelineRenderData render_data;
	PrepareSceneForwardPipelineCommonRenderData(view_id, scene, render_data, pipeline, resources, views, debug_name);
	PrepareSceneForwardPipelineViewDependentRenderData(view_id, view_state, scene, render_data, pipeline, resources, views, debug_name);
	SubmitSceneToForwardPipeline(view_id, scene, rect, view_state, pipeline, render_data, resources, views, aaa, aaa_config, frame, fb, debug_name);
	aaa.Flip(view_state);
}

void SubmitSceneToPipeline(bgfx::ViewId &view_id, const Scene &scene, const Rect<int> &rect, bool fov_axis_is_horizontal, ForwardPipeline &pipeline,
	const PipelineResources &resources, SceneForwardPipelinePassViewId &views, ForwardPipelineAAA &aaa, const ForwardPipelineAAAConfig &aaa_config, int frame,
	bgfx::FrameBufferHandle fb, const char *debug_name) {
	const auto w = float(GetWidth(rect)), h = float(GetHeight(rect));
	const auto ar = fov_axis_is_horizontal ? ComputeAspectRatioX(w, h) : ComputeAspectRatioY(w, h);

	const auto view_state = scene.ComputeCurrentCameraViewState(ar);
	SubmitSceneToPipeline(view_id, scene, rect, view_state, pipeline, resources, views, aaa, aaa_config, frame, fb, debug_name);
}

} // namespace hg
