#include "mungal4.h"
#pragma hdrstop

#include "l4d3d.h"
#include "L4VIDEO.h"
#include "bgfload.h"
#include "image.h"
#include <algorithm>
#include <cstdint>

int gNumBatches = 0;

bool				d3d_OBJECT::mLastTextureScrollState = false;
L4TEXOP::WrapType	d3d_OBJECT::mLastWrapU = L4TEXOP::WrapType::REPEAT;
L4TEXOP::WrapType	d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
bool				d3d_OBJECT::mLastTexturingState = true;
long				d3d_OBJECT::mNextID = 1;
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;

void chgext(char *filePath, const char *newExtension)
{
	int len = strlen(filePath);
	for (int i=len-1; i>=0; i--)
	{
		if (filePath[i] == '.')
		{
			strcpy(&filePath[i + 1], newExtension);
			return;
		}
	}
}

void d3d_OBJECT::ResetState(LPDIRECT3DDEVICE9 device)
{
	d3d_OBJECT::mLastTextureScrollState = false;
	device->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);

	d3d_OBJECT::mLastWrapU = L4TEXOP::WrapType::REPEAT;
	device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);

	d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
	device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);

	//
	// BT port fix: the 2007 port never set filter states, so D3D9 ran at its
	// default D3DTEXF_POINT (nearest-neighbour, no mip selection) -- blocky,
	// shimmering textures regardless of source-art resolution.  The IG board
	// filtered.  Linear min/mag + linear mip (trilinear where mips exist).
	//
	device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
	device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
	device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);

	d3d_OBJECT::mLastTexturingState = true;
	device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
	device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
	device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
}

// DIAG (turn-hitch hunt): time every runtime model load.  New objects entering
// interest range load SYNCHRONOUSLY on the sim thread -- if the momentary
// freezes correlate with these lines, the hitch is lazy model loading.
struct BTLoadTimer
{
	const char *name; LARGE_INTEGER t0;
	BTLoadTimer(const char *n) : name(n) { QueryPerformanceCounter(&t0); }
	~BTLoadTimer()
	{
		LARGE_INTEGER t1, f; QueryPerformanceCounter(&t1); QueryPerformanceFrequency(&f);
		double ms = (double)(t1.QuadPart - t0.QuadPart) * 1000.0 / (double)f.QuadPart;
		if (ms > 5.0)
			DEBUG_STREAM << "[loadobj] " << name << " took " << ms << " ms\n" << std::flush;
	}
};

d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
{
	BTLoadTimer _lt(fileName);
	char fullPath[512];
	sprintf(fullPath, "VIDEO\\%s", fileName);
	chgext(fullPath, "x");

	//Get a ref to the renderer
	DPLRenderer *renderer = l4_application->GetVideoRenderer();

	LPD3DXMESH mesh;
	LPD3DXBUFFER materialBuffer, adjacencyBuffer;
	DWORD materialCount;

	HRESULT hr = D3DXLoadMeshFromXA(fullPath, D3DXMESH_MANAGED, device, &adjacencyBuffer, &materialBuffer, NULL, &materialCount, &mesh);
	if (FAILED(hr))
	{
		// No .x present (the pod content tree ships original .bgf, not exported .x).
		// Try the native .bgf loader before falling back to point-sphere objects.
		d3d_OBJECT *bgf = d3d_OBJECT::LoadObjectBGF(device, fileName);
		if (bgf != NULL)
			return bgf;

		chgext(fullPath, "sph");
		return d3d_OBJECT::LoadSpheres(device, fullPath);
	}

	chgext(fullPath, "det");
	FILE *detFile = fopen(fullPath, "rb");
	int numDetailOps = 0;
	int *detailOps = NULL;

	bool isSky = false;
	
	if (detFile != NULL)
	{
		fread(&numDetailOps, sizeof(int), 1, detFile);

		detailOps = new int[numDetailOps];
		fread(detailOps, sizeof(int), numDetailOps, detFile);
		fclose(detFile);
	}

	chgext(fullPath, "sky");
	FILE *skyFile = fopen(fullPath, "rb");

	if (skyFile != NULL)
	{
		isSky = true;
		fclose(skyFile);
	}

	d3d_OBJECT *object = new d3d_OBJECT(device, mesh, (DWORD*)adjacencyBuffer->GetBufferPointer(), materialCount);

	D3DXMATERIAL *materials = (D3DXMATERIAL*)materialBuffer->GetBufferPointer();

	int nextDetailOp = 0;

	for (DWORD i = 0; i < materialCount; i++)
	{
		chgext(fullPath, "BGF");

		string path(fullPath);

		string actualPath = path.substr(path.find("\\") + 1);
		std::transform(actualPath.begin(), actualPath.end(), actualPath.begin(), toupper);
		const char *matName = opMaterialName(actualPath.c_str(), i);
		char *replaceMatName = NULL;
		char *replaceTexName = NULL;

		if (matName != NULL)
		{
			char *tmpMatName = new char[strlen(matName) + 1];
			strcpy_s(tmpMatName, strlen(matName) + 1, matName);

			replaceMatName = substituteMaterial(tmpMatName);
		}

		object->mDrawOps[i].material = materials[i].MatD3D;

		if (replaceMatName != NULL)
		{
			hash_map<string, ReplacementMaterialData>::const_iterator iter = gReplacementData->find(string(replaceMatName));

			ReplacementMaterialData data = (*iter).second;

			object->mDrawOps[i].material.Diffuse.r = data.r;
			object->mDrawOps[i].material.Diffuse.g = data.g;
			object->mDrawOps[i].material.Diffuse.b = data.b;

			replaceTexName = new char[data.texName.size() + 1];
			strcpy_s(replaceTexName, data.texName.size() + 1, data.texName.c_str());

			delete [] replaceMatName;
		}

		//TODO: set ambient of material; doesn't come back from .X file
		object->mDrawOps[i].material.Ambient = object->mDrawOps[i].material.Diffuse;

		object->mDrawOps[i].texture.texture = NULL;
		if (materials[i].pTextureFilename || replaceTexName)
		{
			char textureFilename[512];

			if (replaceTexName)
			{
				sprintf(textureFilename, "VIDEO\\%s.png", replaceTexName);
				delete [] replaceTexName;
			} else
			{
				sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
			}
			object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
		}

		if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
		{
			object->mDrawOps[i].drawAsDecal = true;
			nextDetailOp++;
		}

		if (abs(object->mDrawOps[i].material.Diffuse.a - 1.0f) > 0.0001f)
		{
			object->mDrawOps[i].alphaTest = true;
		}

		object->mDrawOps[i].drawAsSky = isSky;

		if (isSky)
		{
			object->mDrawOps[i].material.Ambient.r *= renderer->GetCloudRed();
			object->mDrawOps[i].material.Ambient.g *= renderer->GetCloudGreen();
			object->mDrawOps[i].material.Ambient.b *= renderer->GetCloudBlue();
			object->mDrawOps[i].material.Diffuse.r *= renderer->GetCloudRed();
			object->mDrawOps[i].material.Diffuse.g *= renderer->GetCloudGreen();
			object->mDrawOps[i].material.Diffuse.b *= renderer->GetCloudBlue();
			object->mDrawOps[i].material.Emissive.r = renderer->GetCloudEmitRed();
			object->mDrawOps[i].material.Emissive.g = renderer->GetCloudEmitGreen();
			object->mDrawOps[i].material.Emissive.b = renderer->GetCloudEmitBlue();
		}
	}

	materialBuffer->Release();

	return object;
}

// Native .BGF (DIV-BIZ2) mesh loader. The pod content tree ships original .bgf models
// (no exported .x), so when D3DXLoadMeshFromXA fails we build the ID3DXMesh ourselves
// from the .bgf using the proven port loader logic (bgfload.cpp + image.cpp).
d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
{
	BgfData data;
	if (!LoadBgfFile(fileName ? std::string(fileName) : std::string(), data) || data.batches.empty())
		return NULL;

	// SKY DOME detection (task #20): the world's sky is a *sky.bgf dome
	// (dbase=dsky, arena=sky, polar=psky ...) authored with a cloud TEXTURE +
	// white vertex colours.  The original renderer drew it in a dedicated SKY
	// pass (fullbright, minimal fog); our BGF loader route defaults drawAsSky
	// to false, so the dome fell into PASS_OPAQUE and was LIT + FOGGED like
	// terrain -> the "dark navy half-lit dome" the pod never showed.  Tag any
	// *sky* object so its drawOps route to PASS_SKY (lighting-exempt).
	bool isSkyObj = false;
	if (fileName != NULL)
	{
		char lower[64]; int li = 0;
		for (const char *s = fileName; *s && li < 63; ++s)
			lower[li++] = (char)tolower((unsigned char)*s);
		lower[li] = 0;
		if (strstr(lower, "sky") != NULL)
			isSkyObj = true;
	}

	DWORD numFaces = (DWORD)(data.indices.size() / 3);
	DWORD numVerts = (DWORD)data.verts.size();
	if (numFaces == 0 || numVerts == 0)
		return NULL;

	LPD3DXMESH mesh = NULL;
	HRESULT hr = D3DXCreateMeshFVF(numFaces, numVerts,
		D3DXMESH_MANAGED | D3DXMESH_32BIT, L4VERTEX_FVF, device, &mesh);
	if (FAILED(hr) || mesh == NULL)
		return NULL;

	void *vptr = NULL;
	if (SUCCEEDED(mesh->LockVertexBuffer(0, &vptr)) && vptr)
	{
		memcpy(vptr, data.verts.data(), (size_t)numVerts * sizeof(BgfVtx));
		mesh->UnlockVertexBuffer();
	}

	void *iptr = NULL;
	if (SUCCEEDED(mesh->LockIndexBuffer(0, &iptr)) && iptr)
	{
		memcpy(iptr, data.indices.data(), data.indices.size() * sizeof(unsigned int));
		mesh->UnlockIndexBuffer();
	}

	// one attribute id per face = its batch (material) index
	DWORD *attr = NULL;
	if (SUCCEEDED(mesh->LockAttributeBuffer(0, &attr)) && attr)
	{
		for (size_t bi = 0; bi < data.batches.size(); ++bi)
		{
			DWORD startFace = data.batches[bi].indexStart / 3;
			DWORD faceCount = data.batches[bi].indexCount / 3;
			for (DWORD fct = 0; fct < faceCount; ++fct)
				attr[startFace + fct] = (DWORD)bi;
		}
		mesh->UnlockAttributeBuffer();
	}

	// PORT (draw-cost fix): NO GenerateAdjacency/OptimizeInplace here.  The BGF
	// loader's double-sided triangles (each face emitted twice) made the adjacency
	// degenerate and the ATTRSORT optimize fail silently -> no attribute table ->
	// D3DX DrawSubset scanned the whole attribute buffer PER CALL (~44us x ~1400
	// batches = ~60ms/frame = the 10fps baseline).  The batches are already
	// contiguous, face-sorted index ranges, so SET the attribute table EXPLICITLY
	// (D3DXConcatenateMeshes -- the static-world consolidation -- REQUIRES one on
	// its inputs; without it the whole terrain got skipped), and each draw op also
	// carries its range so DrawMesh can DrawIndexedPrimitive directly.
	int drawOpCount = (int)data.batches.size();
	{
		std::vector<D3DXATTRIBUTERANGE> atable((size_t)drawOpCount);
		for (int bi = 0; bi < drawOpCount; ++bi)
		{
			atable[bi].AttribId    = (DWORD)bi;
			atable[bi].FaceStart   = data.batches[bi].indexStart / 3;
			atable[bi].FaceCount   = data.batches[bi].indexCount / 3;
			atable[bi].VertexStart = 0;
			atable[bi].VertexCount = numVerts;
		}
		mesh->SetAttributeTable(atable.data(), (DWORD)drawOpCount);
	}
	d3d_OBJECT *object = new d3d_OBJECT(device, mesh, NULL, drawOpCount);

	// DIAG (BT_LOD_LOG): stash the model basename for flip telemetry
	{
		const char *bn = fileName ? fileName : "";
		for (const char *p = bn; *p; ++p) if (*p == '\\' || *p == '/') bn = p + 1;
		strncpy(object->mDbgName, bn, sizeof(object->mDbgName) - 1);
		object->mDbgName[sizeof(object->mDbgName) - 1] = '\0';
	}

	// BAKED GROUND-SHADOW models (e.g. MECHMOVS.BGF = two coplanar shadow_mtl
	// quads at y=0.1 under the wreck props): drawn opaque they z-fight each
	// other / the floor ("the floor tile under the wreckage flickers"), and
	// look wrong (solid black tiles).  A model whose EVERY batch uses a shadow
	// material is routed through the mech-shadow pipeline: mIsShadow draw
	// (translucent dark, depth-biased, no z-write -- cannot fight) in the
	// blend pass (alphaTest routes the ops there), excluded from static
	// consolidation (RecurseStaticObject).  Mixed models (a vehicle with a
	// baked shadow part among solid parts) are left untouched.
	{
		bool allShadow = !data.batches.empty();
		for (size_t bi = 0; bi < data.batches.size() && allShadow; ++bi)
			if (!data.batches[bi].shadowMat) allShadow = false;
		if (allShadow)
		{
			object->SetIsShadow(1);
			DEBUG_STREAM << "[shadowobj] " << object->mDbgName
				<< " ops=" << (int)data.batches.size() << " -> shadow pipeline\n" << std::flush;
		}
	}

	// cache the mesh's own buffers for the direct-draw path (AddRef'd; dtor releases)
	mesh->GetVertexBuffer(&object->mBgfVB);
	mesh->GetIndexBuffer((LPDIRECT3DINDEXBUFFER9 *)&object->mBgfIB);
	object->mBgfStride   = mesh->GetNumBytesPerVertex();
	object->mBgfNumVerts = mesh->GetNumVertices();

	// DIAG (BT_LOD_LOG): banded-op census at load
	{
		static int s_bandLog = -1;
		if (s_bandLog < 0) { const char *lv = getenv("BT_LOD_LOG"); s_bandLog = (lv != 0 && lv[0] == '1') ? 1 : 0; }
		if (s_bandLog)
		{
			int nBanded = 0;
			for (size_t bi = 0; bi < data.batches.size(); ++bi)
				if (data.batches[bi].lodFar > 0.0f && data.batches[bi].lodFar < 1.0e8f) ++nBanded;
			if (nBanded > 0)
				DEBUG_STREAM << "[lodband] " << object->mDbgName << " ops=" << (int)data.batches.size()
					<< " banded=" << nBanded << "\n" << std::flush;
		}
	}

	// RAMP colorize gate (task #20; BT_RAMP=0 disables for A/B).  The IG board
	// coloured terrain by the material's 2-endpoint ramp (texture luminance ->
	// low..high gradient).  Materials with NO diffuse (grass_mtl) relied on it and
	// fell back to the gray placeholder -> the "pale ground that doesn't match the
	// warm mountains".  When a batch has a ramp we bake lerp(lo,hi,texLum) into the
	// texture and draw with a WHITE material so the ramp colour shows directly.
	static int s_ramp = -1;
	if (s_ramp < 0) { const char *rv = getenv("BT_RAMP"); s_ramp = (rv == 0 || rv[0] != '0') ? 1 : 0; }

	for (int i = 0; i < drawOpCount; i++)
	{
		// direct-draw range for this batch (bypasses D3DX DrawSubset)
		object->mDrawOps[i].bgfStartIndex = (int)data.batches[i].indexStart;
		object->mDrawOps[i].bgfPrimCount  = (int)(data.batches[i].indexCount / 3);
		// authentic LOD band (0x2046): DrawMesh selects by camera distance
		object->mDrawOps[i].lodNear = data.batches[i].lodNear;
		object->mDrawOps[i].lodFar  = data.batches[i].lodFar;
		object->mDrawOps[i].lodDepthBias = data.batches[i].lodBias;

		const bool useRamp = (s_ramp && data.batches[i].hasRamp);
		uint32_t c = data.batches[i].color;
		// PURE-EMISSIVE materials (diffuse black + emissive set, tag 0x26 --
		// btpolar:pintBIceEmit_mtl, the polar glowing ice mounds): authored as
		// an UNLIT self-glow, tex x emissive.  Diffuse/ambient stay black so
		// the sun/ambient add nothing; the (ramped) texture is coloured by the
		// Emissive term alone.
		const bool pureEmissive =
			data.batches[i].hasEmissive && (c & 0x00FFFFFF) == 0;
		D3DMATERIAL9 &m = object->mDrawOps[i].material;
		memset(&m, 0, sizeof(m));
		if (pureEmissive)
		{
			m.Emissive.r = data.batches[i].emissive[0];
			m.Emissive.g = data.batches[i].emissive[1];
			m.Emissive.b = data.batches[i].emissive[2];
		}
		else
		{
			// Ramped batches arrive with batch.color already set to their tint:
			// WHITE for coloured ramps (sky/rock/grass -- the ramp-baked texture
			// shows untinted, the pre-BSL-decode behavior), or the material
			// diffuse for NEUTRAL-ramp skins (the mech's violet-gray limb
			// variants / green lgo6 leg logo / near-white searchlights -- see
			// the RAMP TINT RULE in bgfload.cpp buildPmesh).
			m.Diffuse.r = ((c >> 16) & 0xFF) / 255.0f;
			m.Diffuse.g = ((c >> 8) & 0xFF) / 255.0f;
			m.Diffuse.b = (c & 0xFF) / 255.0f;
		}
		m.Diffuse.a = 1.0f;
		m.Ambient = m.Diffuse;	// matches the .x path; lit by scene ambient even w/o direct lights

		object->mDrawOps[i].alphaTest = object->mIsShadow != 0;	// shadow -> blend pass
		object->mDrawOps[i].drawAsDecal = false;
		object->mDrawOps[i].drawAsSky = isSkyObj;	// sky dome -> PASS_SKY (fullbright)
		// PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels
		// become alpha-0 holes below; DrawMesh alpha-TESTS them in the opaque pass.
		static int s_punch = -1;
		if (s_punch < 0) { const char *pv = getenv("BT_PUNCH"); s_punch = (pv == 0 || pv[0] != '0') ? 1 : 0; }
		object->mDrawOps[i].punchThrough = (s_punch && data.batches[i].punch);
		object->mDrawOps[i].texture.texture = NULL;
		object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT;
		object->mDrawOps[i].texture.wrap_v = L4TEXOP::REPEAT;
		object->mDrawOps[i].texture.doScroll = false;

		const std::string &tp = data.batches[i].texPath;
		if (!tp.empty())
		{
			// texChannel: which BSL bit-slice this texture samples (BMF tag 0x18;
			// mech skins pack 4+ grayscale sheets per .BSL).  Ignored for VTX/TGA.
			Image img = decodeImage(tp, data.batches[i].texChannel);
			if (img.ok && img.w > 0 && img.h > 0)
			{
				LPDIRECT3DTEXTURE9 tex = NULL;
				// BT port fix: was 1 mip level / no usage -- no mipmaps existed, so
				// distant surfaces aliased at any filter mode.  Auto-generate the
				// full mip chain (falls back to a single level if unsupported).
				// PUNCH textures build their chain MANUALLY below (mip alpha must be
				// re-binarized per level -- box-filtered alpha hovers around the test
				// threshold, so texels flicker in/out as the camera distance shifts
				// the mip level: the "walls render with noise / diagonal patterns on
				// slight movement" on the arena framework layers).
				const bool punchTex = object->mDrawOps[i].punchThrough;
				if (punchTex)
				{
					if (FAILED(device->CreateTexture(img.w, img.h, 0, 0,
							D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL)))
						tex = NULL;
				}
				else if (FAILED(device->CreateTexture(img.w, img.h, 0, D3DUSAGE_AUTOGENMIPMAP,
						D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL)))
				{
					tex = NULL;
					device->CreateTexture(img.w, img.h, 1, 0,
						D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL);
				}
				if (tex)
				{
					D3DLOCKED_RECT lr;
					if (SUCCEEDED(tex->LockRect(0, &lr, NULL, 0)))
					{
						// PUNCH keying: black texels of a punch batch become alpha 0
						// (holes).  Keyed on the ORIGINAL texel, before any ramp.
						const bool punchKey = object->mDrawOps[i].punchThrough;
						if (useRamp)
						{
							// Colorize: for each texel, index the low->high ramp by
							// the texel LUMINANCE.  argb = lerp(rampLo, rampHi, lum).
							const float *lo = data.batches[i].rampLo;
							const float *hi = data.batches[i].rampHi;
							for (int y = 0; y < img.h; y++)
							{
								unsigned char *dst = (unsigned char*)lr.pBits + y * lr.Pitch;
								const uint32_t *src = &img.argb[(size_t)y * img.w];
								for (int x = 0; x < img.w; x++)
								{
									uint32_t p = src[x];
									float lum = (0.299f*((p>>16)&0xFF) + 0.587f*((p>>8)&0xFF) + 0.114f*(p&0xFF)) / 255.0f;
									int rr = (int)((lo[0] + (hi[0]-lo[0])*lum) * 255.0f + 0.5f);
									int gg = (int)((lo[1] + (hi[1]-lo[1])*lum) * 255.0f + 0.5f);
									int bb = (int)((lo[2] + (hi[2]-lo[2])*lum) * 255.0f + 0.5f);
									rr = rr<0?0:rr>255?255:rr; gg = gg<0?0:gg>255?255:gg; bb = bb<0?0:bb>255?255:bb;
									uint32_t a = (p & 0xFF000000u);
									if (punchKey && ((p>>16)&0xFF) < 8 && ((p>>8)&0xFF) < 8 && (p&0xFF) < 8)
										a = 0;
									((uint32_t*)dst)[x] = a | (rr<<16) | (gg<<8) | bb;
								}
							}
						}
						else if (punchKey)
						{
							for (int y = 0; y < img.h; y++)
							{
								unsigned char *dst = (unsigned char*)lr.pBits + y * lr.Pitch;
								const uint32_t *src = &img.argb[(size_t)y * img.w];
								for (int x = 0; x < img.w; x++)
								{
									uint32_t p = src[x];
									if (((p>>16)&0xFF) < 8 && ((p>>8)&0xFF) < 8 && (p&0xFF) < 8)
										p &= 0x00FFFFFFu;
									((uint32_t*)dst)[x] = p;
								}
							}
						}
						else
						{
							for (int y = 0; y < img.h; y++)
								memcpy((unsigned char*)lr.pBits + y * lr.Pitch,
									&img.argb[(size_t)y * img.w], (size_t)img.w * 4);
						}
						tex->UnlockRect(0);
					}
					if (punchTex)
					{
						// Manual mip chain with RE-BINARIZED alpha per level: box-
						// filter color from the previous level, then snap alpha to
						// 0/255 at the 0x80 test threshold so the alpha-TEST result
						// is stable across mip transitions (box-filtered alpha
						// hovers around the threshold -> texels flicker in/out as
						// slight camera movement shifts the mip level = the "walls
						// render with noise / diagonal patterns" on the arena
						// framework layers).
						const DWORD nLevels = tex->GetLevelCount();
						for (DWORD lv = 1; lv < nLevels; ++lv)
						{
							D3DLOCKED_RECT lrS, lrD;
							D3DSURFACE_DESC dd, sd;
							tex->GetLevelDesc(lv, &dd);
							tex->GetLevelDesc(lv - 1, &sd);
							if (FAILED(tex->LockRect(lv - 1, &lrS, NULL, D3DLOCK_READONLY))) break;
							if (FAILED(tex->LockRect(lv, &lrD, NULL, 0))) { tex->UnlockRect(lv - 1); break; }
							for (UINT y = 0; y < dd.Height; ++y)
							{
								const UINT sy0 = (y*2   < sd.Height) ? y*2   : sd.Height - 1;
								const UINT sy1 = (y*2+1 < sd.Height) ? y*2+1 : sd.Height - 1;
								uint32_t *dst = (uint32_t*)((unsigned char*)lrD.pBits + y * lrD.Pitch);
								const uint32_t *s0 = (const uint32_t*)((const unsigned char*)lrS.pBits + sy0 * lrS.Pitch);
								const uint32_t *s1 = (const uint32_t*)((const unsigned char*)lrS.pBits + sy1 * lrS.Pitch);
								for (UINT x = 0; x < dd.Width; ++x)
								{
									const UINT sx0 = (x*2   < sd.Width) ? x*2   : sd.Width - 1;
									const UINT sx1 = (x*2+1 < sd.Width) ? x*2+1 : sd.Width - 1;
									const uint32_t p[4] = { s0[sx0], s0[sx1], s1[sx0], s1[sx1] };
									uint32_t a = 0, r = 0, g = 0, b = 0;
									for (int k = 0; k < 4; ++k)
									{
										a += (p[k] >> 24) & 0xFF; r += (p[k] >> 16) & 0xFF;
										g += (p[k] >> 8) & 0xFF;  b += p[k] & 0xFF;
									}
									a /= 4; r /= 4; g /= 4; b /= 4;
									a = (a >= 0x80) ? 0xFF : 0x00;		// re-binarize
									dst[x] = (a << 24) | (r << 16) | (g << 8) | b;
								}
							}
							tex->UnlockRect(lv);
							tex->UnlockRect(lv - 1);
						}
					}
					else
						tex->GenerateMipSubLevels();
					object->mDrawOps[i].texture.texture = tex;
				}
			}
		}
	}

	return object;
}

d3d_OBJECT* d3d_OBJECT::LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName)
{
	FILE *file;
	float emissionColor[3];
	int vertexCount;

	if ((file = fopen(fileName, "rb")) == NULL)
		return NULL;

	fread(emissionColor, sizeof(float), 3, file);
	fread(&vertexCount, sizeof(int), 1, file);

	d3d_OBJECT *object = new d3d_OBJECT(device, vertexCount);
	void *buffer = NULL;

	object->mVB->Lock(0, 0, &buffer, 0);

	fread(buffer, sizeof(L4POINTVERTEX), vertexCount, file);

	D3DXVECTOR3 center(0, 0, 0);
	D3DXComputeBoundingSphere((D3DXVECTOR3*)buffer, vertexCount, sizeof(L4POINTVERTEX), &center, &object->mRadius);

	object->mVB->Unlock();

	object->mDrawOps[0].material.Emissive.r = emissionColor[0];
	object->mDrawOps[0].material.Emissive.g = emissionColor[1];
	object->mDrawOps[0].material.Emissive.b = emissionColor[2];
	object->mDrawOps[0].material.Emissive.a = 1.0f;

	fclose(file);

	return object;
}

L4TEXOP d3d_OBJECT::LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName)
{
	L4TEXOP texOp;
	FILE *file;
	if (mTextureCache.find(fileName) == mTextureCache.end())
	{
		struct
		{	unsigned char minify;
			unsigned char magnify;
			unsigned char alpha;
			unsigned char wrap_u;
			unsigned char wrap_v;
			bool doScroll;
			float scrollUDelta;
			float scrollVDelta;
		} fileTexOp;

		char metFileName[512];
		strcpy(metFileName, fileName);
		chgext(metFileName, "met");

		memset(&texOp, 0, sizeof(L4TEXOP));
		if ((file = fopen(metFileName, "rb")) != NULL)
		{
			fread(&fileTexOp, sizeof(fileTexOp), 1, file);
			fclose(file);

			texOp.wrap_u = (L4TEXOP::WrapType)fileTexOp.wrap_u;
			texOp.wrap_v = (L4TEXOP::WrapType)fileTexOp.wrap_v;
			texOp.doScroll = fileTexOp.doScroll;
			texOp.scrollUDelta = fileTexOp.scrollUDelta;
			texOp.scrollVDelta = fileTexOp.scrollVDelta;
		}
		
		D3DXCreateTextureFromFileA(device, fileName, &texOp.texture);

		mTextureCache.insert(std::pair<std::string,L4TEXOP>(std::string(fileName), texOp));
	}
	else
		texOp = mTextureCache[std::string(fileName)];

	texOp.texture->AddRef();
	return texOp;
}

d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
  : mDevice(device),
	mVertCount(vertCount),
	mDrawOpCount(1),
	mImmune(false),
	mIsShadow(0),
	mVB(NULL),
	mMesh(NULL),
	mDrawOps(NULL),
	mLocalToWorld(),
	mOrigin(),
	mRadius(0.0f),
	mCullCenter(0.0f, 0.0f, 0.0f),
	mCullRadius(-1.0f),
	mBgfVB(NULL),
	mBgfIB(NULL),
	mBgfStride(0),
	mBgfNumVerts(0),
	mID(mNextID++)
{
	HRESULT hr;
	mDbgName[0] = '\0';

	hr = device->CreateVertexBuffer(vertCount * sizeof(L4POINTVERTEX), D3DUSAGE_WRITEONLY, L4POINTVERTEX_FVF, D3DPOOL_MANAGED, &mVB, NULL);

	mDrawOps = new L4DRAWOP();
	memset(mDrawOps, 0, sizeof(L4DRAWOP));

	memset(mNext, 0, sizeof(mNext));
	memset(mPrev, 0, sizeof(mPrev));

	D3DXMatrixIdentity(&mLocalToWorld);
}

d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, LPD3DXMESH mesh, DWORD *adjacencyBuffer, int drawOpCount)
  : mDevice(device),
	mVertCount(-1),
	mDrawOpCount(drawOpCount),
	mImmune(false),
	mIsShadow(0),
	mVB(NULL),
	mMesh(mesh),
	mLocalToWorld(),
	mOrigin(),
	mRadius(0.0f),
	mCullCenter(0.0f, 0.0f, 0.0f),
	mCullRadius(-1.0f),
	mBgfVB(NULL),
	mBgfIB(NULL),
	mBgfStride(0),
	mBgfNumVerts(0),
	mID(mNextID++)
{
	mDbgName[0] = '\0';
	mDrawOps = new L4DRAWOP[drawOpCount];
	memset(mDrawOps, 0, sizeof(L4DRAWOP) * drawOpCount);

	// PORT (frustum culling): compute the model-space bounding sphere once.
	if (mesh != NULL)
	{
		void *vtx = NULL;
		if (SUCCEEDED(mesh->LockVertexBuffer(D3DLOCK_READONLY, &vtx)) && vtx != NULL)
		{
			D3DXVECTOR3 c; FLOAT r = 0.0f;
			if (SUCCEEDED(D3DXComputeBoundingSphere((const D3DXVECTOR3 *)vtx,
					mesh->GetNumVertices(), mesh->GetNumBytesPerVertex(), &c, &r)))
			{
				mCullCenter = c;
				mCullRadius = r;
			}
			mesh->UnlockVertexBuffer();
		}
	}

	if (adjacencyBuffer != NULL)
	{
		//Null adjacency buffer means the mesh should have already been optimized
		mesh->OptimizeInplace(D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_VERTEXCACHE, adjacencyBuffer, NULL, NULL, NULL);
	}

	DWORD vertCount = mesh->GetNumVertices();
	DWORD vertStride = mesh->GetNumBytesPerVertex();
	
	D3DXVECTOR3 *data;
	D3DXVECTOR3 center(0, 0, 0);
	mesh->LockVertexBuffer(D3DLOCK_READONLY, (LPVOID*)&data);
	D3DXComputeBoundingSphere(data, vertCount, vertStride, &center, &mRadius);
	mesh->UnlockVertexBuffer();

	memset(mNext, 0, sizeof(mNext));
	memset(mPrev, 0, sizeof(mPrev));

	D3DXMatrixIdentity(&mLocalToWorld);
}

d3d_OBJECT::~d3d_OBJECT()
{
	if (mVB)
	{
		mVB->Release();
		mVB = NULL;
	}
	if (mBgfVB)
	{
		mBgfVB->Release();
		mBgfVB = NULL;
	}
	if (mBgfIB)
	{
		mBgfIB->Release();
		mBgfIB = NULL;
	}
	if (mDrawOps)
	{
		delete[] mDrawOps;
		mDrawOps = NULL;
	}
}

// PORT (frustum culling): the frame's view frustum, set once per frame by the
// renderer (ExecuteImplementation).  Only the 4 SIDE planes are used -- near/far
// are skipped so the pass-specific projection differences (decal near, sky far)
// can't wrongly cull, and the far-plane fog already hides distant pop.
static D3DXPLANE gCullPlanes[5];
static int       gCullValid = 0;
int              gBTNumCulled = 0;		// per-frame culled-object count (diag; reset by the renderer)
static D3DXVECTOR3 gCamPos(0.0f, 0.0f, 0.0f);	// camera world position (LOD distance selection)

void d3d_OBJECT::SetCameraPosition(float x, float y, float z)
{
	gCamPos.x = x; gCamPos.y = y; gCamPos.z = z;
}

void d3d_OBJECT::SetCullFrustum(const D3DXMATRIX *viewProj)
{
	if (viewProj == NULL) { gCullValid = 0; return; }
	const D3DXMATRIX &m = *viewProj;
	// standard Gribb-Hartmann plane extraction (row-major, v*M convention).
	// 4 side planes + the FAR plane (beyond clip range the GPU clips the object
	// anyway -- skipping it is a guaranteed no-visual-change win; the sky pass is
	// excluded from culling and the decal projection shares the main far plane).
	// The NEAR plane is skipped (pass-specific near differences).
	gCullPlanes[0] = D3DXPLANE(m._14 + m._11, m._24 + m._21, m._34 + m._31, m._44 + m._41);	// left
	gCullPlanes[1] = D3DXPLANE(m._14 - m._11, m._24 - m._21, m._34 - m._31, m._44 - m._41);	// right
	gCullPlanes[2] = D3DXPLANE(m._14 + m._12, m._24 + m._22, m._34 + m._32, m._44 + m._42);	// bottom
	gCullPlanes[3] = D3DXPLANE(m._14 - m._12, m._24 - m._22, m._34 - m._32, m._44 - m._42);	// top
	gCullPlanes[4] = D3DXPLANE(m._14 - m._13, m._24 - m._23, m._34 - m._33, m._44 - m._43);	// far
	for (int i = 0; i < 5; ++i)
		D3DXPlaneNormalize(&gCullPlanes[i], &gCullPlanes[i]);
	gCullValid = 1;
}

void d3d_OBJECT::Draw(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame)
{
	// DIAG (BT_WATCH=<substr>): trace a named model's draw lifecycle -- proves
	// whether a "popping" object stops being drawn (torn down upstream), gets
	// frustum-culled, or draws-but-invisible.  Logs every ~120th call + culls.
	static const char *s_watch = getenv("BT_WATCH");
	int watched = 0;
	if (s_watch && s_watch[0] && mDbgName[0])
	{
		for (const char *p = mDbgName; *p && !watched; ++p)
			if (_strnicmp(p, s_watch, strlen(s_watch)) == 0) watched = 1;
		if (watched)
		{
			static int s_wn = 0;
			if ((++s_wn % 120) == 1)
			{
				const float wdx = mLocalToWorld._41 - gCamPos.x;
				const float wdz = mLocalToWorld._43 - gCamPos.z;
				DEBUG_STREAM << "[watch] " << mDbgName << " id=" << mID
					<< " pass=" << pass << " shadow=" << mIsShadow
					<< " d=" << sqrtf(wdx*wdx + wdz*wdz)
					<< " T=(" << mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43 << ")"
					<< " R0=(" << mLocalToWorld._11 << "," << mLocalToWorld._12 << "," << mLocalToWorld._13 << ")"
					<< " R1=(" << mLocalToWorld._21 << "," << mLocalToWorld._22 << "," << mLocalToWorld._23 << ")"
					<< " R2=(" << mLocalToWorld._31 << "," << mLocalToWorld._32 << "," << mLocalToWorld._33 << ")\n" << std::flush;
			}
		}
	}
	// PORT (frustum culling): skip objects whose world bounding sphere is fully
	// outside the side planes.  Sky is never culled (its dome/plane must always
	// draw); unknown bounds (mCullRadius<=0, e.g. point-sphere objects) never cull.
	if (gCullValid && pass != PASS_SKY && mCullRadius > 0.0f)
	{
		D3DXVECTOR3 wc;
		D3DXVec3TransformCoord(&wc, &mCullCenter, &mLocalToWorld);
		// conservative world radius: scale by the largest basis-row length
		float sx = mLocalToWorld._11*mLocalToWorld._11 + mLocalToWorld._12*mLocalToWorld._12 + mLocalToWorld._13*mLocalToWorld._13;
		float sy = mLocalToWorld._21*mLocalToWorld._21 + mLocalToWorld._22*mLocalToWorld._22 + mLocalToWorld._23*mLocalToWorld._23;
		float sz = mLocalToWorld._31*mLocalToWorld._31 + mLocalToWorld._32*mLocalToWorld._32 + mLocalToWorld._33*mLocalToWorld._33;
		float s2 = sx > sy ? (sx > sz ? sx : sz) : (sy > sz ? sy : sz);
		float wr = mCullRadius * (s2 > 1.0f ? sqrtf(s2) : 1.0f);
		for (int i = 0; i < 5; ++i)
			if (gCullPlanes[i].a * wc.x + gCullPlanes[i].b * wc.y
				+ gCullPlanes[i].c * wc.z + gCullPlanes[i].d < -wr)
			{
				++gBTNumCulled;
				// DIAG (BT_CULL_LOG): dump big culled objects -- bad bounds show as
				// a big radius culled at an implausible world centre.
				if (wr > 40.0f && getenv("BT_CULL_LOG"))
				{
					static int s_cl = 0;
					if (s_cl < 40)
					{
						++s_cl;
						DEBUG_STREAM << "[cull] plane=" << i << " wc=(" << wc.x << ","
							<< wc.y << "," << wc.z << ") wr=" << wr
							<< " local=(" << mCullCenter.x << "," << mCullCenter.y << "," << mCullCenter.z
							<< ") r=" << mCullRadius
							<< " T=(" << mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43
							<< ")\n" << std::flush;
					}
				}
				return;										// fully outside -> skip
			}
	}

	if (pass == PASS_OPAQUE)
	{
		static int dbgDraw = 0;
		if (dbgDraw < 60)
		{
			DEBUG_STREAM << "[DRAW] opaque #" << dbgDraw << " worldT=("
				<< mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43
				<< ") ops=" << mDrawOpCount << " mesh=" << (mMesh != NULL) << "\n" << std::flush;
			++dbgDraw;
		}
	}
	// set our world transform
	mDevice->SetTransform(D3DTS_WORLD, &mLocalToWorld);

	if (mMesh != NULL && pass != PASS_SPHERE)
		return DrawMesh(pass, viewTransform, targetRenderFrame);
	else if (mMesh == NULL && mVB != NULL && pass == PASS_SPHERE)
		return DrawSpheres(pass, viewTransform);
}

void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame)
{
	// BT (task #20): the shadow proxy draws ONLY in the blend pass, as ~55%
	// translucent black (constant TFACTOR color/alpha), no Z-write -- the
	// authentic dark ground silhouette instead of the opaque black blob.
	if (mIsShadow)
	{
		if (pass != PASS_ALPHABLEND)
			return;
		// Save EVERY state we touch and restore it exactly (a leaked LIGHTING
		// FALSE / ALPHAARG1 TFACTOR darkened the whole scene on first attempt).
		DWORD sBlend, sSrc, sDst, sZWrite, sLight, sTFactor;
		DWORD sColorOp, sColorArg1, sAlphaOp, sAlphaArg1;
		DWORD sDepthBias, sSlopeBias;
		mDevice->GetRenderState(D3DRS_ALPHABLENDENABLE, &sBlend);
		mDevice->GetRenderState(D3DRS_SRCBLEND, &sSrc);
		mDevice->GetRenderState(D3DRS_DESTBLEND, &sDst);
		mDevice->GetRenderState(D3DRS_ZWRITEENABLE, &sZWrite);
		mDevice->GetRenderState(D3DRS_LIGHTING, &sLight);
		mDevice->GetRenderState(D3DRS_TEXTUREFACTOR, &sTFactor);
		mDevice->GetRenderState(D3DRS_DEPTHBIAS, &sDepthBias);
		mDevice->GetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, &sSlopeBias);
		mDevice->GetTextureStageState(0, D3DTSS_COLOROP,   &sColorOp);
		mDevice->GetTextureStageState(0, D3DTSS_COLORARG1, &sColorArg1);
		mDevice->GetTextureStageState(0, D3DTSS_ALPHAOP,   &sAlphaOp);
		mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAlphaArg1);

		mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
		mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
		mDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
		mDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
		mDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
		{
			// DIAG (BT_SHADOW_COLOR=AARRGGBB hex): override the shadow tint --
			// e.g. FFFF0000 opaque red proves whether the quad reaches pixels.
			static DWORD s_shadowColor = 0;
			if (s_shadowColor == 0)
			{
				const char *sc = getenv("BT_SHADOW_COLOR");
				s_shadowColor = sc ? (DWORD)strtoul(sc, NULL, 16) : 0x8C000000;
				if (s_shadowColor == 0) s_shadowColor = 0x8C000000;
			}
			mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, s_shadowColor);
		}
		//
		// DEPTH BIAS (task #20 shadow-vanish fix): the shadow proxy sits on the
		// COLLISION surface (the mech origin), but the visual terrain mesh runs up
		// to ~2u ABOVE the coarse collision solid on mound/mesa slopes -- a plain
		// Z-test buries the whole quad there and the shadow VANISHES (and without
		// its ground-contact cue, the authentic small foot-clip reads as "the mech
		// is sinking").  The IG board drew ground shadows decal-style (depth-biased
		// coplanar geometry -- the classic 90s blob-shadow technique; the shadow
		// flicker visible in pod footage is exactly this bias fighting at edges).
		// Pull the shadow toward the camera in depth so it stays ON the terrain
		// wherever the surface runs slightly above the quad.  BT_SHADOW_BIAS
		// overrides (float, default -0.004 normalized depth ~= 2-4 world units at
		// chase-view range on the 2100-far projection); =0 disables (plain Z-test).
		//
		{
			static float s_bias = -1e9f;
			if (s_bias == -1e9f)
			{
				const char *bv = getenv("BT_SHADOW_BIAS");
				s_bias = bv ? (float)atof(bv) : -0.004f;
			}
			mDevice->SetRenderState(D3DRS_DEPTHBIAS, *(DWORD *)&s_bias);
			float slope = s_bias == 0.0f ? 0.0f : -1.0f;	// steep-slope pixels get extra pull
			mDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, *(DWORD *)&slope);
		}
		mDevice->SetTextureStageState(0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
		mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
		mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG1);
		mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
		const float _sdx = mLocalToWorld._41 - gCamPos.x;
		const float _sdy = mLocalToWorld._42 - gCamPos.y;
		const float _sdz = mLocalToWorld._43 - gCamPos.z;
		const float shadowLodDist = sqrtf(_sdx*_sdx + _sdy*_sdy + _sdz*_sdz);
		for (int iOp = 0; iOp < mDrawOpCount; iOp++)
		{
			if (mDrawOps[iOp].lodFar > 0.0f &&
				(shadowLodDist < mDrawOps[iOp].lodNear || shadowLodDist >= mDrawOps[iOp].lodFar))
				continue;							// LOD band (see DrawMesh main loop)
			gNumBatches++;
			if (mDrawOps[iOp].bgfPrimCount > 0 && mBgfVB != NULL)
			{
				mDevice->SetFVF(L4VERTEX_FVF);
				mDevice->SetStreamSource(0, mBgfVB, 0, mBgfStride);
				mDevice->SetIndices(mBgfIB);
				mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts,
					mDrawOps[iOp].bgfStartIndex, mDrawOps[iOp].bgfPrimCount);
			}
			else
				mMesh->DrawSubset(iOp);
		}
		mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, sBlend);
		mDevice->SetRenderState(D3DRS_SRCBLEND, sSrc);
		mDevice->SetRenderState(D3DRS_DESTBLEND, sDst);
		mDevice->SetRenderState(D3DRS_ZWRITEENABLE, sZWrite);
		mDevice->SetRenderState(D3DRS_LIGHTING, sLight);
		mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, sTFactor);
		mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDepthBias);
		mDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, sSlopeBias);
		mDevice->SetTextureStageState(0, D3DTSS_COLOROP,   sColorOp);
		mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, sColorArg1);
		mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP,   sAlphaOp);
		mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAlphaArg1);
		return;
	}
	// ADDITIVE-LOD band gate: skip the ops whose [lodNear..lodFar) band excludes
	// the eye distance (the BGF 0x2046 ranges pre-scaled by sqrt3 at load; see
	// bgfload.cpp TAG_LOD -- under the decoded ADDITIVE rule every banded op
	// carries [0..OutDist), so this is "drop the near-detail LOD once the eye
	// is OutDist away").  lodFar==0 (zero-init non-BGF op) => always drawn.
	// METRIC (decoded, lod-blink-decode workflow): the eye distance to the
	// INSTANCE-TRANSFORMED OBJECT HOT SPOT -- default (0,0,0) = the instance
	// origin (dpl_SetObjectLodHotSpot / s_dplobject.lod_hot_spot; no arena
	// content carries a 0x2047 per-LOD reference).  ONE shared d for ALL LODs
	// of an object, so sibling bands hand off coherently (the OpenFlight "use
	// previous slant range" contract).  The earlier bounding-sphere-SURFACE
	// metric was NOT authentic: per-piece radius offsets desynchronized the
	// shared measurement point (each piece switched at its own position-
	// dependent radius).  With the sqrt3 range decode the authored massing
	// ranges (2000) exceed the arena's reachable distances + clip (1300), so
	// origin ranging no longer drops anything large in play.
	const float _ldx = mLocalToWorld._41 - gCamPos.x;
	const float _ldy = mLocalToWorld._42 - gCamPos.y;
	const float _ldz = mLocalToWorld._43 - gCamPos.z;
	const float lodDist = sqrtf(_ldx*_ldx + _ldy*_ldy + _ldz*_ldz);

	for (int iOp = 0; iOp < mDrawOpCount; iOp++)
	{
		L4DRAWOP *drawOp = &mDrawOps[iOp];

		if (drawOp->alphaTest != (pass == PASS_ALPHABLEND))
			continue;

		if (drawOp->drawAsDecal != (pass == PASS_DECAL))
			continue;

		if (drawOp->drawAsSky != (pass == PASS_SKY))
			continue;

		if (drawOp->lodFar > 0.0f && drawOp->lodFar < 1.0e8f)
		{
			const bool bandVis = !(lodDist < drawOp->lodNear || lodDist >= drawOp->lodFar);
			// DIAG (BT_LOD_LOG): log every band-gate visibility FLIP with the model
			// name + band + distance -- identifies exactly which piece blinks while
			// the mech moves (the user's "objects appear/disappear with position").
			static int s_lodLog = -1;
			if (s_lodLog < 0) { const char *lv = getenv("BT_LOD_LOG"); s_lodLog = (lv != 0 && lv[0] == '1') ? 1 : 0; }
			if (s_lodLog)
			{
				static int s_firstEval = 0;
				if (s_firstEval < 5)
				{
					++s_firstEval;
					DEBUG_STREAM << "[lodgate] eval " << (mDbgName[0] ? mDbgName : "?")
						<< " op" << iOp << " d=" << lodDist << " far=" << drawOp->lodFar << "\n" << std::flush;
				}
				const int nowVis = bandVis ? 1 : 2;
				if (drawOp->lastBandVisible != 0 && drawOp->lastBandVisible != nowVis)
				{
					static int s_flips = 0;
					if (s_flips < 400)
					{
						++s_flips;
						DEBUG_STREAM << "[lodflip] " << (mDbgName[0] ? mDbgName : "?")
							<< " op" << iOp << (bandVis ? " IN " : " OUT ")
							<< "band=[" << drawOp->lodNear << ".." << drawOp->lodFar
							<< ") d=" << lodDist << "\n" << std::flush;
					}
				}
				drawOp->lastBandVisible = nowVis;
			}
			if (!bandVis)
				continue;							// outside this LOD's viewing band
		}

		mDevice->SetMaterial(&drawOp->material);

#ifndef RP3_EMULATE
		SetTextureScrolling(&(drawOp->texture), targetRenderFrame);
		SetTexture(drawOp->texture.texture);
#endif

		// PUNCH (dpl_Punchize): cutout op -- alpha-TEST the hole texels (alpha 0,
		// keyed at load) in the opaque pass, z-write preserved.  States restored
		// right after the draw so ordinary ops are untouched.
		DWORD sAT = 0, sARef = 0, sAFunc = 0;
		if (drawOp->punchThrough)
		{
			mDevice->GetRenderState(D3DRS_ALPHATESTENABLE, &sAT);
			mDevice->GetRenderState(D3DRS_ALPHAREF, &sARef);
			mDevice->GetRenderState(D3DRS_ALPHAFUNC, &sAFunc);
			mDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE);
			mDevice->SetRenderState(D3DRS_ALPHAREF, 0x80);
			mDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL);
		}
		// LAYER DEPTH BIAS (additive-LOD coplanar resolution; see l4d3d.h):
		// finer layers pulled a few depth-buffer steps toward the camera so
		// flush duplicated surfaces resolve deterministically to the detail
		// layer (the board's submission-order rule) instead of z-fighting.
		DWORD sDB = 0;
		if (drawOp->lodDepthBias != 0.0f)
		{
			mDevice->GetRenderState(D3DRS_DEPTHBIAS, &sDB);
			mDevice->SetRenderState(D3DRS_DEPTHBIAS, *(const DWORD *)&drawOp->lodDepthBias);
		}

		gNumBatches++;
		if (drawOp->bgfPrimCount > 0 && mBgfVB != NULL)
		{
			// direct-draw: known contiguous range, no attribute-table scan.
			// (DrawSubset used to set the vertex decl itself -- assert the FVF here
			// since beam/HUD passes change it mid-frame; runtime filters redundancy.)
			mDevice->SetFVF(L4VERTEX_FVF);
			mDevice->SetStreamSource(0, mBgfVB, 0, mBgfStride);
			mDevice->SetIndices(mBgfIB);
			mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts,
				drawOp->bgfStartIndex, drawOp->bgfPrimCount);
		}
		else
			mMesh->DrawSubset(iOp);

		if (drawOp->punchThrough)
		{
			mDevice->SetRenderState(D3DRS_ALPHATESTENABLE, sAT);
			mDevice->SetRenderState(D3DRS_ALPHAREF, sARef);
			mDevice->SetRenderState(D3DRS_ALPHAFUNC, sAFunc);
		}
		if (drawOp->lodDepthBias != 0.0f)
			mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDB);
	}
}

void d3d_OBJECT::DrawSpheres(int pass, const D3DXMATRIX *viewTransform)
{
	mDevice->SetFVF(L4POINTVERTEX_FVF);
	mDevice->SetStreamSource(0, mVB, 0, sizeof(L4POINTVERTEX));
	mDevice->SetMaterial(&mDrawOps[0].material);
	SetTexture(NULL);
	mDevice->DrawPrimitive(D3DPT_POINTLIST, 0, mVertCount);
}

void d3d_OBJECT::SetTextureScrolling(const L4TEXOP *texture, Time targetRenderFrame)
{
	if (d3d_OBJECT::mLastTextureScrollState != texture->doScroll)
	{
		// the texture scrolling state has changed
		if (texture->doScroll)
		{
			mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
			SetTextureAddressing(texture->wrap_u, texture->wrap_v);
		}
		else
		{
			mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
			SetTextureAddressing(L4TEXOP::WrapType::REPEAT, L4TEXOP::WrapType::REPEAT);
		}
		d3d_OBJECT::mLastTextureScrollState = texture->doScroll;
	}

	// if the texture is supposed to be scrolled then
	// we have to set the transforms regardless of state
	if (texture->doScroll)
	{
		Scalar time = (Scalar)targetRenderFrame;

		D3DXMATRIX translate;
		D3DXMatrixIdentity(&translate);
		translate._31 = -texture->scrollUDelta * time;
		translate._32 = texture->scrollVDelta * time;
		mDevice->SetTransform(D3DTS_TEXTURE0, &translate);
	} 
}

void d3d_OBJECT::SetTextureAddressing(L4TEXOP::WrapType wrap_u, L4TEXOP::WrapType wrap_v)
{
	if (wrap_u != d3d_OBJECT::mLastWrapU)
	{
		d3d_OBJECT::mLastWrapU = wrap_u;
		if (wrap_u == L4TEXOP::REPEAT)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
		else if (wrap_u == L4TEXOP::CLAMP)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
		else if (wrap_u == L4TEXOP::SELECT)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER);
	}
	if (wrap_v != d3d_OBJECT::mLastWrapV)
	{
		d3d_OBJECT::mLastWrapV = wrap_v;
		if (wrap_v == L4TEXOP::REPEAT)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
		else if (wrap_v == L4TEXOP::CLAMP)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
		else if (wrap_v == L4TEXOP::SELECT)
			mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER);
	}
}

void d3d_OBJECT::SetTexture(LPDIRECT3DTEXTURE9 texture)
{
	bool texturingOn = (texture != NULL);
	if (texturingOn != d3d_OBJECT::mLastTexturingState)
	{
		d3d_OBJECT::mLastTexturingState = texturingOn;
		if (texturingOn)
		{
			mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
			mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
			mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
		}
		else
			mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE);
	}
	if (texture != NULL)
	{
		mDevice->SetTexture(0, texture);
	}
}