#include "d3drmrenderer.h" #include "d3drmrenderer_software.h" #include "ddsurface_impl.h" #include "mathutils.h" #include "miniwin.h" #include #include #include #include #include Direct3DRMSoftwareRenderer::Direct3DRMSoftwareRenderer(DWORD width, DWORD height) : m_width(width), m_height(height) { m_zBuffer.resize(m_width * m_height); } void Direct3DRMSoftwareRenderer::PushLights(const SceneLight* lights, size_t count) { m_lights.assign(lights, lights + count); } void Direct3DRMSoftwareRenderer::SetProjection(const D3DRMMATRIX4D& projection, D3DVALUE front, D3DVALUE back) { m_front = front; m_back = back; memcpy(m_projection, projection, sizeof(D3DRMMATRIX4D)); } void Direct3DRMSoftwareRenderer::ClearZBuffer() { std::fill(m_zBuffer.begin(), m_zBuffer.end(), std::numeric_limits::infinity()); } void Direct3DRMSoftwareRenderer::ProjectVertex(const GeometryVertex& v, D3DRMVECTOR4D& p) const { float px = m_projection[0][0] * v.position.x + m_projection[1][0] * v.position.y + m_projection[2][0] * v.position.z + m_projection[3][0]; float py = m_projection[0][1] * v.position.x + m_projection[1][1] * v.position.y + m_projection[2][1] * v.position.z + m_projection[3][1]; float pz = m_projection[0][2] * v.position.x + m_projection[1][2] * v.position.y + m_projection[2][2] * v.position.z + m_projection[3][2]; float pw = m_projection[0][3] * v.position.x + m_projection[1][3] * v.position.y + m_projection[2][3] * v.position.z + m_projection[3][3]; p.w = pw; // Perspective divide if (pw != 0.0f) { px /= pw; py /= pw; pz /= pw; } // Map from NDC [-1,1] to screen coordinates p.x = (px * 0.5f + 0.5f) * m_width; p.y = (1.0f - (py * 0.5f + 0.5f)) * m_height; p.z = pz; } GeometryVertex SplitEdge(GeometryVertex a, const GeometryVertex& b, float plane) { float t = (plane - a.position.z) / (b.position.z - a.position.z); a.position.x = a.position.x + t * (b.position.x - a.position.x); a.position.y = a.position.y + t * (b.position.y - a.position.y); a.position.z = plane; a.texCoord.u = a.texCoord.u + t * (b.texCoord.u - a.texCoord.u); a.texCoord.v = a.texCoord.v + t * (b.texCoord.v - a.texCoord.v); a.normals.x = a.normals.x + t * (b.normals.x - a.normals.x); a.normals.y = a.normals.y + t * (b.normals.y - a.normals.y); a.normals.z = a.normals.z + t * (b.normals.z - a.normals.z); float len = std::sqrt(a.normals.x * a.normals.x + a.normals.y * a.normals.y + a.normals.z * a.normals.z); if (len > 0.0001f) { a.normals.x /= len; a.normals.y /= len; a.normals.z /= len; } return a; } void Direct3DRMSoftwareRenderer::DrawTriangleClipped(const GeometryVertex (&v)[3], const Appearance& appearance) { bool in0 = v[0].position.z >= m_front; bool in1 = v[1].position.z >= m_front; bool in2 = v[2].position.z >= m_front; int insideCount = in0 + in1 + in2; if (insideCount == 0) { return; } if (insideCount == 3) { DrawTriangleProjected(v[0], v[1], v[2], appearance); } else if (insideCount == 2) { GeometryVertex split; if (!in0) { split = SplitEdge(v[2], v[0], m_front); DrawTriangleProjected(v[1], v[2], split, appearance); DrawTriangleProjected(v[1], split, SplitEdge(v[1], v[0], m_front), appearance); } else if (!in1) { split = SplitEdge(v[0], v[1], m_front); DrawTriangleProjected(v[2], v[0], split, appearance); DrawTriangleProjected(v[2], split, SplitEdge(v[2], v[1], m_front), appearance); } else { split = SplitEdge(v[1], v[2], m_front); DrawTriangleProjected(v[0], v[1], split, appearance); DrawTriangleProjected(v[0], split, SplitEdge(v[0], v[2], m_front), appearance); } } else if (in0) { DrawTriangleProjected(v[0], SplitEdge(v[0], v[1], m_front), SplitEdge(v[0], v[2], m_front), appearance); } else if (in1) { DrawTriangleProjected(SplitEdge(v[1], v[0], m_front), v[1], SplitEdge(v[1], v[2], m_front), appearance); } else { DrawTriangleProjected(SplitEdge(v[2], v[0], m_front), SplitEdge(v[2], v[1], m_front), v[2], appearance); } } /** * @todo pre-compute a blending table when running in 256 colors since the game always uses an alpha of 152 */ void Direct3DRMSoftwareRenderer::BlendPixel(Uint8* pixelAddr, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { Uint32 dstPixel = 0; memcpy(&dstPixel, pixelAddr, m_bytesPerPixel); Uint8 dstR, dstG, dstB, dstA; SDL_GetRGBA(dstPixel, m_format, m_palette, &dstR, &dstG, &dstB, &dstA); float alpha = a / 255.0f; float invAlpha = 1.0f - alpha; Uint8 outR = static_cast(r * alpha + dstR * invAlpha); Uint8 outG = static_cast(g * alpha + dstG * invAlpha); Uint8 outB = static_cast(b * alpha + dstB * invAlpha); Uint8 outA = static_cast(a + dstA * invAlpha); Uint32 blended = SDL_MapRGBA(m_format, m_palette, outR, outG, outB, outA); memcpy(pixelAddr, &blended, m_bytesPerPixel); } SDL_Color Direct3DRMSoftwareRenderer::ApplyLighting(const GeometryVertex& vertex, const Appearance& appearance) { FColor specular = {0, 0, 0, 0}; FColor diffuse = {0, 0, 0, 0}; // Position and normal D3DVECTOR position = vertex.position; D3DVECTOR normal = vertex.normals; float normLen = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z); if (normLen == 0.0f) { return appearance.color; } normal.x /= normLen; normal.y /= normLen; normal.z /= normLen; for (const auto& light : m_lights) { FColor lightColor = light.color; if (light.positional == 0.0f && light.directional == 0.0f) { // Ambient light diffuse.r += lightColor.r; diffuse.g += lightColor.g; diffuse.b += lightColor.b; continue; } // TODO lightVec only has to be calculated once per frame for directional lights D3DVECTOR lightVec; if (light.directional == 1.0f) { lightVec = {-light.direction.x, -light.direction.y, -light.direction.z}; } else if (light.positional == 1.0f) { lightVec = {light.position.x - position.x, light.position.y - position.y, light.position.z - position.z}; } float len = std::sqrt(lightVec.x * lightVec.x + lightVec.y * lightVec.y + lightVec.z * lightVec.z); if (len == 0.0f) { continue; } lightVec.x /= len; lightVec.y /= len; lightVec.z /= len; float dotNL = normal.x * lightVec.x + normal.y * lightVec.y + normal.z * lightVec.z; if (dotNL > 0.0f) { // Diffuse contribution diffuse.r += dotNL * lightColor.r; diffuse.g += dotNL * lightColor.g; diffuse.b += dotNL * lightColor.b; if (appearance.shininess != 0.0f) { // Using dotNL ignores view angle, but this matches DirectX 5 behavior. float spec = std::pow(dotNL, appearance.shininess); specular.r += spec * lightColor.r; specular.g += spec * lightColor.g; specular.b += spec * lightColor.b; } } } return SDL_Color{ static_cast(std::min(255.0f, diffuse.r * appearance.color.r + specular.r * 255.0f)), static_cast(std::min(255.0f, diffuse.g * appearance.color.g + specular.g * 255.0f)), static_cast(std::min(255.0f, diffuse.b * appearance.color.b + specular.b * 255.0f)), appearance.color.a }; } void Direct3DRMSoftwareRenderer::DrawTriangleProjected( const GeometryVertex& v0, const GeometryVertex& v1, const GeometryVertex& v2, const Appearance& appearance ) { D3DRMVECTOR4D p0, p1, p2; ProjectVertex(v0, p0); ProjectVertex(v1, p1); ProjectVertex(v2, p2); // Skip triangles outside the frustum if ((p0.z < m_front && p1.z < m_front && p2.z < m_front) || (p0.z > m_back && p1.z > m_back && p2.z > m_back)) { return; } // Skip offscreen triangles if ((p0.x < 0 && p1.x < 0 && p2.x < 0) || (p0.x >= m_width && p1.x >= m_width && p2.x >= m_width) || (p0.y < 0 && p1.y < 0 && p2.y < 0) || (p0.y >= m_height && p1.y >= m_height && p2.y >= m_height)) { return; } int minX = std::max(0, (int) std::floor(std::min({p0.x, p1.x, p2.x}))); int maxX = std::min((int) m_width - 1, (int) std::ceil(std::max({p0.x, p1.x, p2.x}))); int minY = std::max(0, (int) std::floor(std::min({p0.y, p1.y, p2.y}))); int maxY = std::min((int) m_height - 1, (int) std::ceil(std::max({p0.y, p1.y, p2.y}))); if (minX > maxX || minY > maxY) { return; } auto edge = [](double x0, double y0, double x1, double y1, double x, double y) { return (x - x0) * (y1 - y0) - (y - y0) * (x1 - x0); }; float area = edge(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y); if (area >= 0) { return; } float invArea = 1.0f / area; // Per-vertex lighting using vertex normals SDL_Color c0 = ApplyLighting(v0, appearance); SDL_Color c1 = ApplyLighting(v1, appearance); SDL_Color c2 = ApplyLighting(v2, appearance); Uint32 textureId = appearance.textureId; int texturePitch; Uint8* texels = nullptr; int texWidthScale; int texHeightScale; if (textureId != NO_TEXTURE_ID) { SDL_Surface* texture = m_textures[textureId].cached; if (texture) { texturePitch = texture->pitch; texels = static_cast(texture->pixels); texWidthScale = texture->w - 1; texHeightScale = texture->h - 1; } } Uint8* pixels = (Uint8*) DDBackBuffer->pixels; int pitch = DDBackBuffer->pitch; for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { float px = x + 0.5f; float py = y + 0.5f; float w0 = edge(p1.x, p1.y, p2.x, p2.y, px, py) * invArea; if (w0 < 0.0f || w0 > 1.0f) { continue; } float w1 = edge(p2.x, p2.y, p0.x, p0.y, px, py) * invArea; if (w1 < 0.0f || w1 > 1.0f - w0) { continue; } float w2 = 1.0f - w0 - w1; float z = w0 * p0.z + w1 * p1.z + w2 * p2.z; int zidx = y * m_width + x; float& zref = m_zBuffer[zidx]; if (z >= zref) { continue; } // Interpolate color Uint8 r = static_cast(w0 * c0.r + w1 * c1.r + w2 * c2.r); Uint8 g = static_cast(w0 * c0.g + w1 * c1.g + w2 * c2.g); Uint8 b = static_cast(w0 * c0.b + w1 * c1.b + w2 * c2.b); Uint8* pixelAddr = pixels + y * pitch + x * m_bytesPerPixel; if (appearance.color.a == 255) { zref = z; if (texels) { // Perspective correct interpolate texture coords float invW = w0 / p0.w + w1 / p1.w + w2 / p2.w; if (invW == 0.0) { continue; } invW = 1.0 / invW; float u = static_cast( ((w0 * v0.texCoord.u / p0.w) + (w1 * v1.texCoord.u / p1.w) + (w2 * v2.texCoord.u / p2.w)) * invW ); float v = static_cast( ((w0 * v0.texCoord.v / p0.w) + (w1 * v1.texCoord.v / p1.w) + (w2 * v2.texCoord.v / p2.w)) * invW ); // Tile textures u = u - std::floor(u); v = v - std::floor(v); int texX = static_cast(u * texWidthScale); int texY = static_cast(v * texHeightScale); Uint8* texelAddr = texels + texY * texturePitch + texX * m_bytesPerPixel; Uint32 texelColor = 0; memcpy(&texelColor, texelAddr, m_bytesPerPixel); Uint8 tr, tg, tb, ta; SDL_GetRGBA(texelColor, m_format, m_palette, &tr, &tg, &tb, &ta); // Multiply vertex color by texel color r = (r * tr + 127) / 255; g = (g * tg + 127) / 255; b = (b * tb + 127) / 255; } Uint32 finalColor = SDL_MapRGBA(m_format, m_palette, r, g, b, 255); memcpy(pixelAddr, &finalColor, m_bytesPerPixel); } else { // Transparent alpha blending with vertex alpha BlendPixel(pixelAddr, r, g, b, appearance.color.a); } } } } struct TextureDestroyContext { Direct3DRMSoftwareRenderer* renderer; Uint32 textureId; }; void Direct3DRMSoftwareRenderer::AddTextureDestroyCallback(Uint32 id, IDirect3DRMTexture* texture) { auto* ctx = new TextureDestroyContext{this, id}; texture->AddDestroyCallback( [](IDirect3DRMObject* obj, void* arg) { auto* ctx = static_cast(arg); auto& cacheEntry = ctx->renderer->m_textures[ctx->textureId]; if (cacheEntry.cached) { SDL_UnlockSurface(cacheEntry.cached); SDL_DestroySurface(cacheEntry.cached); cacheEntry.cached = nullptr; cacheEntry.texture = nullptr; } delete ctx; }, ctx ); } Uint32 Direct3DRMSoftwareRenderer::GetTextureId(IDirect3DRMTexture* iTexture) { auto texture = static_cast(iTexture); auto surface = static_cast(texture->m_surface); // Check if already mapped for (Uint32 i = 0; i < m_textures.size(); ++i) { auto& texRef = m_textures[i]; if (texRef.texture == texture) { if (texRef.version != texture->m_version) { // Update animated textures SDL_DestroySurface(texRef.cached); texRef.cached = SDL_ConvertSurface(surface->m_surface, DDBackBuffer->format); SDL_LockSurface(texRef.cached); texRef.version = texture->m_version; } return i; } } SDL_Surface* convertedRender = SDL_ConvertSurface(surface->m_surface, DDBackBuffer->format); SDL_LockSurface(convertedRender); // Reuse freed slot for (Uint32 i = 0; i < m_textures.size(); ++i) { auto& texRef = m_textures[i]; if (texRef.texture == nullptr) { texRef.texture = texture; texRef.cached = convertedRender; texRef.version = texture->m_version; AddTextureDestroyCallback(i, texture); return i; } } // Append new m_textures.push_back({texture, texture->m_version, convertedRender}); AddTextureDestroyCallback(static_cast(m_textures.size() - 1), texture); return static_cast(m_textures.size() - 1); } DWORD Direct3DRMSoftwareRenderer::GetWidth() { return m_width; } DWORD Direct3DRMSoftwareRenderer::GetHeight() { return m_height; } void Direct3DRMSoftwareRenderer::GetDesc(D3DDEVICEDESC* halDesc, D3DDEVICEDESC* helDesc) { memset(halDesc, 0, sizeof(D3DDEVICEDESC)); helDesc->dcmColorModel = D3DCOLORMODEL::RGB; helDesc->dwFlags = D3DDD_DEVICEZBUFFERBITDEPTH; helDesc->dwDeviceZBufferBitDepth = DDBD_32; helDesc->dwDeviceRenderBitDepth = DDBD_8 | DDBD_16 | DDBD_24 | DDBD_32; helDesc->dpcTriCaps.dwTextureCaps = D3DPTEXTURECAPS_PERSPECTIVE; helDesc->dpcTriCaps.dwShadeCaps = D3DPSHADECAPS_ALPHAFLATBLEND; helDesc->dpcTriCaps.dwTextureFilterCaps = D3DPTFILTERCAPS_LINEAR; } const char* Direct3DRMSoftwareRenderer::GetName() { return "Miniwin Emulation"; } HRESULT Direct3DRMSoftwareRenderer::BeginFrame(const D3DRMMATRIX4D& viewMatrix) { if (!DDBackBuffer || !SDL_LockSurface(DDBackBuffer)) { return DDERR_GENERIC; } ClearZBuffer(); memcpy(m_viewMatrix, viewMatrix, sizeof(D3DRMMATRIX4D)); m_format = SDL_GetPixelFormatDetails(DDBackBuffer->format); m_palette = SDL_GetSurfacePalette(DDBackBuffer); m_bytesPerPixel = m_format->bits_per_pixel / 8; return DD_OK; } void Direct3DRMSoftwareRenderer::SubmitDraw( const GeometryVertex* vertices, const size_t count, const D3DRMMATRIX4D& worldMatrix, const Matrix3x3& normalMatrix, const Appearance& appearance ) { D3DRMMATRIX4D mvMatrix; MultiplyMatrix(mvMatrix, worldMatrix, m_viewMatrix); for (size_t i = 0; i + 2 < count; i += 3) { GeometryVertex vrts[3]; for (size_t j = 0; j < 3; ++j) { const GeometryVertex& src = vertices[i + j]; vrts[j].position = TransformPoint(src.position, mvMatrix); vrts[j].normals = Normalize(TransformNormal(src.normals, normalMatrix)); vrts[j].texCoord = src.texCoord; } DrawTriangleClipped(vrts, appearance); } } HRESULT Direct3DRMSoftwareRenderer::FinalizeFrame() { SDL_UnlockSurface(DDBackBuffer); return DD_OK; }