// ---------------------------------------------------------------------------
//  M88 カスタマイズプロジェクト
//  Copyright (C) 2026 M88 カスタマイズプロジェクト.
//  2条項BSDライセンスの下で配布する。
// ---------------------------------------------------------------------------
//	Direct3D 11 による画面描画 (CRT フィルタ用)
//
//	★ レンダーターゲットのフォーマットは全て _UNORM を使うこと。_SRGB は禁止。
//	   Pass2 で使う crt.hlsl はシェーダ内で手動ガンマ変換 (pow 2.2) を行う前提で
//	   書かれている。RT が _SRGB だとハードウェアの sRGB デコード/エンコードが
//	   自動で掛かり、シェーダ内の手動変換と二重になって絵が破綻する
//	   (白飛び、または暗く沈む)。詳細は docs/crt-shader-port.md §7。
//	   このファイルで作る RT・スワップチェーン・テクスチャは
//	   すべて DXGI_FORMAT_R8G8B8A8_UNORM を使うこと。
// ---------------------------------------------------------------------------

#include "headers.h"
#include "drawd3d.h"
#include <d3dcompiler.h>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")

template <class T> static void SafeRelease(T** ppT)
{
	if (*ppT)
	{
		(*ppT)->Release();
		*ppT = 0;
	}
}

// ---------------------------------------------------------------------------
//	構築/消滅
//
WinDrawD3D::WinDrawD3D()
	: m_hWnd(0), m_hCWnd(0), m_width(640), m_height(400),
	  m_dev(0), m_ctx(0), m_swap(0), m_backbufferRTV(0),
	  m_indexTex(0), m_indexSRV(0), m_paletteTex(0), m_paletteSRV(0),
	  m_rgbTex(0), m_rgbRTV(0), m_rgbSRV(0),
	  m_vsFullscreen(0), m_psPalette(0), m_psCRT(0), m_cbCRT(0), m_sampPoint(0),
	  m_image(0), m_bpl(SRCW), m_paletteDirty(false), m_imageDirty(false)
{
	memset(m_palette, 0, sizeof(m_palette));

	// SetCRTParams() が呼ばれるまでの既定値。
	// docs/crt-shader-port.md §4 の「わりとよい」値に合わせておく
	m_effectParams.enable = false;
	m_effectParams.scanline = 0.36f;
	m_effectParams.vignette = 0.05f;
	m_effectParams.aberration = 0.35f;
	m_effectParams.curvature = 0.005f;
	m_effectParams.smoothstep = true;
	m_effectParams.scanlineNativeHeight = 400.0f;

	// エミュレータが Lock() で直接書き込む CPU 側バッファ。
	// GPU テクスチャを直接 Lock で返さない (DrawGDI.cpp / DrawD2D.cpp と同じ流儀)。
	// エミュレーションスレッドから呼ばれるため、D3D デバイスコンテキストとの
	// スレッド安全性が問題になる
	m_image = new uint8[SRCW * SRCH];
	memset(m_image, 0x40, SRCW * SRCH);
}

WinDrawD3D::~WinDrawD3D()
{
	Cleanup();
	delete[] m_image;
}

// ---------------------------------------------------------------------------
//	初期化処理
//	w/h は出力サイズ(表示面の大きさ)。転送元サイズ(640x400)ではない
//
bool WinDrawD3D::Init(HWND hwindow, uint w, uint h, GUID*)
{
	m_hWnd = hwindow;

	if (m_hCWnd == 0)
	{
		m_hCWnd = ::CreateWindowEx(
			WS_EX_TRANSPARENT, "M88p2 WinUI", "", WS_CHILD,
			0, 0, w, h, m_hWnd, NULL, NULL, NULL);
	}
	if (m_hCWnd == 0)
		return false;
	::ShowWindow(m_hCWnd, SW_SHOW);

	if (!CreateDevice())
		return false;
	if (!CreateSwapChain(w, h))
		return false;
	if (!CreateBackBufferRTV())
		return false;
	if (!CreateResources())
		return false;
	if (!CompileShaders())
		return false;

	m_width = w;
	m_height = h;
	status |= Draw::shouldrefresh;
	return true;
}

// ---------------------------------------------------------------------------
//	D3D11 デバイスの作成
//
bool WinDrawD3D::CreateDevice()
{
	D3D_FEATURE_LEVEL levels[] =
	{
		D3D_FEATURE_LEVEL_11_0,
		D3D_FEATURE_LEVEL_10_1,
		D3D_FEATURE_LEVEL_10_0,
	};
	D3D_FEATURE_LEVEL obtained;

	HRESULT hr = D3D11CreateDevice(
		NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0,
		levels, ARRAYSIZE(levels), D3D11_SDK_VERSION,
		&m_dev, &obtained, &m_ctx);

	return SUCCEEDED(hr);
}

// ---------------------------------------------------------------------------
//	スワップチェーンの作成
//	w/h は出力サイズ(子ウインドウのクライアントサイズ)
//
bool WinDrawD3D::CreateSwapChain(uint w, uint h)
{
	IDXGIDevice* dxgidev = 0;
	IDXGIAdapter* adapter = 0;
	IDXGIFactory* factory = 0;

	HRESULT hr = m_dev->QueryInterface(__uuidof(IDXGIDevice), (void**) &dxgidev);
	if (SUCCEEDED(hr))
		hr = dxgidev->GetAdapter(&adapter);
	if (SUCCEEDED(hr))
		hr = adapter->GetParent(__uuidof(IDXGIFactory), (void**) &factory);

	bool result = false;
	if (SUCCEEDED(hr))
	{
		DXGI_SWAP_CHAIN_DESC desc;
		memset(&desc, 0, sizeof(desc));
		desc.BufferDesc.Width = w;
		desc.BufferDesc.Height = h;
		desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;	// _UNORM 必須。_SRGB 禁止
		desc.BufferDesc.RefreshRate.Numerator = 0;
		desc.BufferDesc.RefreshRate.Denominator = 1;
		desc.SampleDesc.Count = 1;
		desc.SampleDesc.Quality = 0;
		desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
		desc.OutputWindow = m_hCWnd;
		desc.Windowed = TRUE;
		desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
		desc.BufferCount = 2;

		hr = factory->CreateSwapChain(m_dev, &desc, &m_swap);
		if (FAILED(hr))
		{
			// 古い環境向けフォールバック (Windows 8 以前など)
			desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
			desc.BufferCount = 1;
			hr = factory->CreateSwapChain(m_dev, &desc, &m_swap);
		}
		result = SUCCEEDED(hr);

		if (result)
		{
			// DXGI 独自の Alt+Enter 排他フルスクリーン化を禁止する。
			// 全画面はボーダーレス窓 (WinUI::ApplyWindowGeometry) で実現しているため、
			// SetFullscreenState 経路を使わせない (docs/03-phase3-crt.md Step 3-4)
			factory->MakeWindowAssociation(m_hCWnd,
				DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
		}
	}

	SafeRelease(&factory);
	SafeRelease(&adapter);
	SafeRelease(&dxgidev);
	return result;
}

// ---------------------------------------------------------------------------
//	バックバッファの RenderTargetView を作成
//
bool WinDrawD3D::CreateBackBufferRTV()
{
	ID3D11Texture2D* backbuffer = 0;
	HRESULT hr = m_swap->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**) &backbuffer);
	if (FAILED(hr))
		return false;

	hr = m_dev->CreateRenderTargetView(backbuffer, NULL, &m_backbufferRTV);
	backbuffer->Release();
	return SUCCEEDED(hr);
}

// ---------------------------------------------------------------------------
//	出力サイズを変更する
//	width/height は子ウインドウ(=表示面)の大きさ。転送元は 640x400 固定
//
//	★ UI スレッド専用 (WinDraw::Resize と同じ制約)。
//	   子ウインドウを作ったスレッドから SetWindowPos するため
//
bool WinDrawD3D::Resize(uint width, uint height)
{
	if (!m_swap)
		return false;

	m_width = width;
	m_height = height;

	::SetWindowPos(m_hCWnd, HWND_BOTTOM, 0, 0, m_width, m_height, SWP_SHOWWINDOW);

	// ResizeBuffers の前に RTV を含む全参照を解放しておく必要がある。
	// ★ SafeRelease で C++ 側の参照カウントを落とすだけでは足りない。
	//   直前の RenderPass1 の OMSetRenderTargets でバックバッファの RTV を
	//   デバイスコンテキストへ結合したままだと、コンテキスト内部が
	//   まだ参照を持っているため ResizeBuffers が DXGI_ERROR_INVALID_CALL で
	//   失敗する。スワップチェーンのサイズが変わらないまま返るため、
	//   ウインドウ枠だけ拡大されて表示内容は旧サイズで左上に残る
	//   (実機で確認された不具合。2026-08-19)
	m_ctx->OMSetRenderTargets(0, NULL, NULL);
	SafeRelease(&m_backbufferRTV);

	HRESULT hr = m_swap->ResizeBuffers(0, m_width, m_height, DXGI_FORMAT_UNKNOWN, 0);
	if (FAILED(hr))
		return false;

	if (!CreateBackBufferRTV())
		return false;

	status |= Draw::shouldrefresh;
	return true;
}

// ---------------------------------------------------------------------------
//	後片付け
//
bool WinDrawD3D::Cleanup()
{
	SafeRelease(&m_cbCRT);
	SafeRelease(&m_sampPoint);
	SafeRelease(&m_psCRT);
	SafeRelease(&m_psPalette);
	SafeRelease(&m_vsFullscreen);
	SafeRelease(&m_rgbSRV);
	SafeRelease(&m_rgbRTV);
	SafeRelease(&m_rgbTex);
	SafeRelease(&m_paletteSRV);
	SafeRelease(&m_paletteTex);
	SafeRelease(&m_indexSRV);
	SafeRelease(&m_indexTex);
	SafeRelease(&m_backbufferRTV);
	SafeRelease(&m_swap);
	SafeRelease(&m_ctx);
	SafeRelease(&m_dev);

	if (m_hCWnd)
	{
		::DestroyWindow(m_hCWnd);
		m_hCWnd = 0;
	}
	return true;
}

// ---------------------------------------------------------------------------
//	パレット設定
//	index 番目のパレットに pal をセット
//
void WinDrawD3D::SetPalette(PALETTEENTRY* pal, int index, int nentries)
{
	for (; nentries > 0; nentries--)
	{
		m_palette[index] = *pal;
		index++, pal++;
	}
	m_paletteDirty = true;
}

// ---------------------------------------------------------------------------
//	CRT フィルタのパラメータを設定する。
//	実際の定数バッファへの反映は次の RenderPass2 (描画スレッド側) で行う
//
void WinDrawD3D::SetCRTParams(const CRTEffectParams& params)
{
	m_effectParams = params;
}

// ---------------------------------------------------------------------------
//	Index/Palette テクスチャとサンプラの作成
//
bool WinDrawD3D::CreateResources()
{
	D3D11_TEXTURE2D_DESC td;
	memset(&td, 0, sizeof(td));
	td.Width = SRCW;
	td.Height = SRCH;
	td.MipLevels = 1;
	td.ArraySize = 1;
	td.Format = DXGI_FORMAT_R8_UNORM;
	td.SampleDesc.Count = 1;
	td.Usage = D3D11_USAGE_DYNAMIC;
	td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	td.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	HRESULT hr = m_dev->CreateTexture2D(&td, NULL, &m_indexTex);
	if (FAILED(hr))
		return false;
	hr = m_dev->CreateShaderResourceView(m_indexTex, NULL, &m_indexSRV);
	if (FAILED(hr))
		return false;

	D3D11_TEXTURE2D_DESC pd;
	memset(&pd, 0, sizeof(pd));
	pd.Width = 256;
	pd.Height = 1;
	pd.MipLevels = 1;
	pd.ArraySize = 1;
	pd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;	// _UNORM 必須。パレットは色空間変換をしない (palette.hlsl 冒頭参照)
	pd.SampleDesc.Count = 1;
	pd.Usage = D3D11_USAGE_DYNAMIC;
	pd.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	pd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	hr = m_dev->CreateTexture2D(&pd, NULL, &m_paletteTex);
	if (FAILED(hr))
		return false;
	hr = m_dev->CreateShaderResourceView(m_paletteTex, NULL, &m_paletteSRV);
	if (FAILED(hr))
		return false;

	D3D11_SAMPLER_DESC sd;
	memset(&sd, 0, sizeof(sd));
	// ★ Point 必須。Linear で補間すると存在しないパレット番号が生成され色が破綻する
	sd.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
	// ★ Clamp 必須。Wrap にすると画面端が反対側から回り込む
	sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
	sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
	sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
	sd.ComparisonFunc = D3D11_COMPARISON_NEVER;
	sd.MaxLOD = D3D11_FLOAT32_MAX;

	hr = m_dev->CreateSamplerState(&sd, &m_sampPoint);
	if (FAILED(hr))
		return false;

	// Pass1 の出力 = Pass2 の入力。640x400 固定 (出力サイズには依存しない)。
	// _UNORM 必須 (ファイル冒頭コメント参照)
	D3D11_TEXTURE2D_DESC rd;
	memset(&rd, 0, sizeof(rd));
	rd.Width = SRCW;
	rd.Height = SRCH;
	rd.MipLevels = 1;
	rd.ArraySize = 1;
	rd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	rd.SampleDesc.Count = 1;
	rd.Usage = D3D11_USAGE_DEFAULT;
	rd.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;

	hr = m_dev->CreateTexture2D(&rd, NULL, &m_rgbTex);
	if (FAILED(hr))
		return false;
	hr = m_dev->CreateRenderTargetView(m_rgbTex, NULL, &m_rgbRTV);
	if (FAILED(hr))
		return false;
	hr = m_dev->CreateShaderResourceView(m_rgbTex, NULL, &m_rgbSRV);
	if (FAILED(hr))
		return false;

	// crt.hlsl の cbuffer CRTParams (b0)。64バイト固定 (DrawD3D.h の static_assert 参照)
	D3D11_BUFFER_DESC cd;
	memset(&cd, 0, sizeof(cd));
	cd.ByteWidth = sizeof(CRTParams);
	cd.Usage = D3D11_USAGE_DYNAMIC;
	cd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	hr = m_dev->CreateBuffer(&cd, NULL, &m_cbCRT);
	return SUCCEEDED(hr);
}

// ---------------------------------------------------------------------------
//	シェーダファイルの場所を探す
//	開発中に再ビルド無しで調整できるよう、外部 .hlsl を実行時にコンパイルする
//	(docs/03-phase3-crt.md Step 3-1)。
//
//	M88.exe は ROM を置いた任意のフォルダにコピーして実行される運用のため
//	(CLAUDE.md 参照)、リポジトリ内の相対パスだけには頼れない。
//	ビルド時に PostBuildEvent (M88.vcxproj) が src\win32\shaders\*.hlsl を
//	$(OutDir)shaders\ へコピーしているので、まず exe と同じ場所の
//	shaders\ を探す。exe をリポジトリの外にコピーして使う場合は、
//	この shaders フォルダも一緒にコピーする必要がある。
//	リポジトリのビルドツリー(x64\Release\M88.exe など)から直接実行している
//	場合向けに、コピー忘れでも動くよう ..\..\src\win32\shaders\ もフォールバックで見る
//
std::wstring WinDrawD3D::FindShaderPath(const wchar_t* filename)
{
	wchar_t modpath[MAX_PATH];
	GetModuleFileNameW(NULL, modpath, MAX_PATH);

	std::wstring dir = modpath;
	size_t pos = dir.find_last_of(L"\\/");
	if (pos != std::wstring::npos)
		dir = dir.substr(0, pos);

	std::wstring nextToExe = dir + L"\\shaders\\" + filename;
	if (GetFileAttributesW(nextToExe.c_str()) != INVALID_FILE_ATTRIBUTES)
		return nextToExe;

	return dir + L"\\..\\..\\src\\win32\\shaders\\" + filename;
}

// ---------------------------------------------------------------------------
//	1本の VS と 2本の PS (palette.hlsl の VSMain/PSPalette、
//	crt.hlsl の PSMain) を実行時コンパイルする。
//	*vs/*palettePS/*crtPS は呼び出し側が初期化しておく必要はない
//	(このメソッドが新規に Create するオブジェクトを returns する)。
//	失敗した場合、途中まで作られたオブジェクトはこのメソッド内で解放し、
//	出力ポインタは全て NULL のまま返す (呼び出し側の状態を汚さない)
//
bool WinDrawD3D::CompileShaderSet
(ID3D11VertexShader** vs, ID3D11PixelShader** palettePS, ID3D11PixelShader** crtPS)
{
	*vs = 0;
	*palettePS = 0;
	*crtPS = 0;

	std::wstring palettePath = FindShaderPath(L"palette.hlsl");
	std::wstring crtPath = FindShaderPath(L"crt.hlsl");

	ID3DBlob* blob = 0;
	ID3DBlob* err = 0;

	HRESULT hr = D3DCompileFromFile(palettePath.c_str(), NULL, NULL,
		"VSMain", "vs_5_0", 0, 0, &blob, &err);
	if (FAILED(hr))
	{
		if (err)
		{
			OutputDebugStringA((char*) err->GetBufferPointer());
			err->Release();
		}
		return false;
	}
	hr = m_dev->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), NULL, vs);
	blob->Release();
	if (FAILED(hr))
		return false;

	hr = D3DCompileFromFile(palettePath.c_str(), NULL, NULL,
		"PSPalette", "ps_5_0", 0, 0, &blob, &err);
	if (FAILED(hr))
	{
		if (err)
		{
			OutputDebugStringA((char*) err->GetBufferPointer());
			err->Release();
		}
		SafeRelease(vs);
		return false;
	}
	hr = m_dev->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), NULL, palettePS);
	blob->Release();
	if (FAILED(hr))
	{
		SafeRelease(vs);
		return false;
	}

	hr = D3DCompileFromFile(crtPath.c_str(), NULL, NULL,
		"PSMain", "ps_5_0", 0, 0, &blob, &err);
	if (FAILED(hr))
	{
		if (err)
		{
			OutputDebugStringA((char*) err->GetBufferPointer());
			err->Release();
		}
		SafeRelease(vs);
		SafeRelease(palettePS);
		return false;
	}
	hr = m_dev->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), NULL, crtPS);
	blob->Release();
	if (FAILED(hr))
	{
		SafeRelease(vs);
		SafeRelease(palettePS);
		return false;
	}

	return true;
}

// ---------------------------------------------------------------------------
//	palette.hlsl (Pass1) と crt.hlsl (Pass2) を実行時コンパイルする
//
bool WinDrawD3D::CompileShaders()
{
	return CompileShaderSet(&m_vsFullscreen, &m_psPalette, &m_psCRT);
}

// ---------------------------------------------------------------------------
//	外部 .hlsl を読み直す (メニュー「Shader > Reload」から呼ばれる)。
//	コンパイルに失敗しても現在使用中のシェーダはそのまま残し、
//	クラッシュせず false を返す (docs/03-phase3-crt.md 動作確認手順 19)
//
bool WinDrawD3D::ReloadShader()
{
	if (!m_dev)
		return false;

	ID3D11VertexShader* newVS = 0;
	ID3D11PixelShader* newPalettePS = 0;
	ID3D11PixelShader* newCrtPS = 0;

	if (!CompileShaderSet(&newVS, &newPalettePS, &newCrtPS))
		return false;

	SafeRelease(&m_vsFullscreen);
	SafeRelease(&m_psPalette);
	SafeRelease(&m_psCRT);
	m_vsFullscreen = newVS;
	m_psPalette = newPalettePS;
	m_psCRT = newCrtPS;
	return true;
}

// ---------------------------------------------------------------------------
//	CPU 側バッファ(m_image/m_palette) を GPU テクスチャへ転送する
//	Map/Unmap は DrawScreen (描画スレッド側) から呼ぶこと。
//	Lock/Unlock はエミュレーションスレッドから呼ばれるため、
//	ここで GPU リソースを直接触ってはいけない
//
void WinDrawD3D::UpdateTextures()
{
	if (m_imageDirty)
	{
		D3D11_MAPPED_SUBRESOURCE mapped;
		HRESULT hr = m_ctx->Map(m_indexTex, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
		if (SUCCEEDED(hr))
		{
			uint8* src = m_image;
			uint8* dst = (uint8*) mapped.pData;
			for (uint y = 0; y < SRCH; y++)
			{
				memcpy(dst, src, SRCW);
				src += m_bpl;
				dst += mapped.RowPitch;
			}
			m_ctx->Unmap(m_indexTex, 0);
		}
		m_imageDirty = false;
	}

	if (m_paletteDirty)
	{
		D3D11_MAPPED_SUBRESOURCE mapped;
		HRESULT hr = m_ctx->Map(m_paletteTex, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
		if (SUCCEEDED(hr))
		{
			uint8* dst = (uint8*) mapped.pData;
			for (int i = 0; i < 256; i++)
			{
				dst[i*4+0] = m_palette[i].peRed;
				dst[i*4+1] = m_palette[i].peGreen;
				dst[i*4+2] = m_palette[i].peBlue;
				// peFlags をアルファに使わない。パレットは常に不透明
				dst[i*4+3] = 255;
			}
			m_ctx->Unmap(m_paletteTex, 0);
		}
		m_paletteDirty = false;
	}
}

// ---------------------------------------------------------------------------
//	Pass1: パレット展開。オフスクリーンRT (640x400 固定) へ描画する
//
void WinDrawD3D::RenderPass1()
{
	D3D11_VIEWPORT vp;
	vp.TopLeftX = 0.0f;
	vp.TopLeftY = 0.0f;
	vp.Width = (FLOAT) SRCW;
	vp.Height = (FLOAT) SRCH;
	vp.MinDepth = 0.0f;
	vp.MaxDepth = 1.0f;
	m_ctx->RSSetViewports(1, &vp);

	m_ctx->OMSetRenderTargets(1, &m_rgbRTV, NULL);

	m_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	m_ctx->VSSetShader(m_vsFullscreen, NULL, 0);
	m_ctx->PSSetShader(m_psPalette, NULL, 0);

	ID3D11ShaderResourceView* srvs[2] = { m_indexSRV, m_paletteSRV };
	m_ctx->PSSetShaderResources(0, 2, srvs);
	m_ctx->PSSetSamplers(0, 1, &m_sampPoint);

	// 頂点バッファ不要 (VSMain が SV_VertexID からフルスクリーン三角形を生成する)
	m_ctx->Draw(3, 0);
}

// ---------------------------------------------------------------------------
//	出力面 (m_width x m_height) に 640:400 (8:5) で内接する矩形を求める
//	窓モードでは出力面ちょうどになり、全画面や非8:5倍率では上下または左右に
//	余白(レターボックス)が出る。WinDrawD2D::ComputeDestRect と同じ計算。
//	整数倍には丸めない (docs/03-phase3-crt.md Step 3-1)
//
void WinDrawD3D::ComputeDestRect(RECT& rect)
{
	int w = (int) m_width;
	int h = (int) m_width * (int) SRCH / (int) SRCW;
	if (h > (int) m_height)
	{
		h = (int) m_height;
		w = (int) m_height * (int) SRCW / (int) SRCH;
	}

	rect.left   = ((int) m_width  - w) / 2;
	rect.top    = ((int) m_height - h) / 2;
	rect.right  = rect.left + w;
	rect.bottom = rect.top  + h;
}

// ---------------------------------------------------------------------------
//	m_effectParams と出力サイズから CRTParams (定数バッファ) を組み立てて転送する
//
void WinDrawD3D::UpdateCRTParamsBuffer()
{
	CRTParams p;
	p.SourceSize[0] = (float) SRCW;
	p.SourceSize[1] = (float) SRCH;
	p.SourceSizeInv[0] = 1.0f / SRCW;
	p.SourceSizeInv[1] = 1.0f / SRCH;
	p.OutputSize[0] = (float) m_width;
	p.OutputSize[1] = (float) m_height;
	p.OutputSizeInv[0] = m_width  ? 1.0f / m_width  : 0.0f;
	p.OutputSizeInv[1] = m_height ? 1.0f / m_height : 0.0f;
	p.ScanlineStrength = m_effectParams.scanline;
	p.VignetteStrength = m_effectParams.vignette;
	p.AberrationPx = m_effectParams.aberration;
	p.CurvatureAmount = m_effectParams.curvature;
	p.ScanlineNativeHeight = m_effectParams.scanlineNativeHeight;
	p.SmoothstepEnabled = m_effectParams.smoothstep ? 1.0f : 0.0f;
	p.MasterEnable = m_effectParams.enable ? 1.0f : 0.0f;
	p.pad0 = 0.0f;

	D3D11_MAPPED_SUBRESOURCE mapped;
	HRESULT hr = m_ctx->Map(m_cbCRT, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
	if (SUCCEEDED(hr))
	{
		memcpy(mapped.pData, &p, sizeof(p));
		m_ctx->Unmap(m_cbCRT, 0);
	}
}

// ---------------------------------------------------------------------------
//	Pass2: オフスクリーンRTをバックバッファへ拡大描画する (CRTエフェクト)。
//	バックバッファ全体を黒でクリアしてからレターボックス矩形だけに描画する
//	(はみ出た領域が黒帯として残る)
//
void WinDrawD3D::RenderPass2()
{
	UpdateCRTParamsBuffer();

	m_ctx->OMSetRenderTargets(1, &m_backbufferRTV, NULL);

	static const float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
	m_ctx->ClearRenderTargetView(m_backbufferRTV, black);

	RECT dest;
	ComputeDestRect(dest);

	D3D11_VIEWPORT vp;
	vp.TopLeftX = (FLOAT) dest.left;
	vp.TopLeftY = (FLOAT) dest.top;
	vp.Width = (FLOAT) (dest.right - dest.left);
	vp.Height = (FLOAT) (dest.bottom - dest.top);
	vp.MinDepth = 0.0f;
	vp.MaxDepth = 1.0f;
	m_ctx->RSSetViewports(1, &vp);

	m_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	m_ctx->VSSetShader(m_vsFullscreen, NULL, 0);
	m_ctx->PSSetShader(m_psCRT, NULL, 0);

	m_ctx->PSSetShaderResources(0, 1, &m_rgbSRV);
	m_ctx->PSSetSamplers(0, 1, &m_sampPoint);
	m_ctx->PSSetConstantBuffers(0, 1, &m_cbCRT);

	m_ctx->Draw(3, 0);

	// m_rgbTex は次の RenderPass1 で再び RTV として結合される。
	// SRV として結合されたまま残らないよう明示的に外しておく
	ID3D11ShaderResourceView* nullSRV = NULL;
	m_ctx->PSSetShaderResources(0, 1, &nullSRV);
}

// ---------------------------------------------------------------------------
//	描画
//
void WinDrawD3D::DrawScreen(const RECT&, bool)
{
	if (!m_ctx || !m_swap || !m_backbufferRTV)
		return;

	UpdateTextures();
	RenderPass1();
	RenderPass2();

	// Sequencer が独自にタイミングを取っているため、SyncInterval=0 で
	// vsync 待ちによるエミュレーションスレッドのブロックを避ける
	m_swap->Present(0, 0);
}

// ---------------------------------------------------------------------------
//	画面イメージの使用要求
//
bool WinDrawD3D::Lock(uint8** pimage, int* pbpl)
{
	*pimage = m_image;
	*pbpl = m_bpl;
	return m_image != 0;
}

// ---------------------------------------------------------------------------
//	画面イメージの使用終了
//
bool WinDrawD3D::Unlock()
{
	m_imageDirty = true;
	status &= ~Draw::shouldrefresh;
	return true;
}
