diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 4c09844adbb..afc5f8d0833 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -31,6 +31,7 @@ #include "WW3D2/pointgr.h" #include "WW3D2/streak.h" #include "WW3D2/rinfo.h" +#include "WW3D2/textureatlas.h" #include "WWLib/bittype.h" class W3DTerrainParticles; @@ -51,9 +52,11 @@ class W3DParticleSystemManager : public ParticleSystemManager ///< returns the number of particles shown on screen per frame virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } + virtual void reset() override; + private: - Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture); - void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture); + Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture, const ShaderClass& shader); + void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture, const ShaderClass& shader); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); enum { MAX_POINTS_PER_GROUP = 512 }; @@ -66,9 +69,11 @@ class W3DParticleSystemManager : public ParticleSystemManager ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha ShareBufferClass *m_sizeBuffer; ///< array of particle sizes ShareBufferClass *m_angleBuffer; ///< array of particle orientations + TextureAtlasClass m_atlas; ///< pages shared by particles with different textures + Vector4 m_uvRects[MAX_POINTS_PER_GROUP]; ///< array of particle texture rectangles, for atlas pages AABoxClass m_currentBoundingBox; - ParticleSystemInfo::ParticleShaderType m_batchShaderType; + ShaderClass m_batchShader; ///< the shader used for batched particle draws Bool m_readyToRender; ///< if true, it is OK to render ParticleSystemInfo::ParticleAlignmentType m_batchParticleAlignment; }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 08713b76fc9..37094652ee3 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -44,11 +44,57 @@ //------------------------------------------------------------------------------------------------- +static Bool supportsAtlasing(const ParticleSystem& system) +{ + return system.isUsingParticles() && system.getParticleAlignment() != ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING; +} + +static Bool usesSharedBlend(ParticleSystemInfo::ParticleShaderType shaderType, Bool atlased) +{ + return atlased && (shaderType == ParticleSystemInfo::ALPHA || shaderType == ParticleSystemInfo::ADDITIVE); +} + +static ShaderClass getParticleShader(ParticleSystemInfo::ParticleShaderType shaderType, Bool sharedBlend) +{ + if (sharedBlend) + { + ShaderClass shader = ShaderClass::_PresetAlphaSpriteShader; + shader.Set_Src_Blend_Func(ShaderClass::SRCBLEND_ONE); + return shader; + } + + switch (shaderType) + { + case ParticleSystemInfo::ADDITIVE: + return ShaderClass::_PresetAdditiveSpriteShader; + case ParticleSystemInfo::ALPHA: + return ShaderClass::_PresetAlphaSpriteShader; + case ParticleSystemInfo::ALPHA_TEST: + return ShaderClass::_PresetATestSpriteShader; + case ParticleSystemInfo::MULTIPLY: + return ShaderClass::_PresetMultiplicativeSpriteShader; + default: + return ShaderClass(); + } +} + +// With the shared blend, additive sprites get zero alpha so they add, and alpha sprites a premultiplied color. +// The color is clamped first, as the fixed-function pipeline clamps it before the blend weighs it by alpha. +static Vector4 getVertexColor(const RGBColor& color, Real alpha, ParticleSystemInfo::ParticleShaderType shaderType, Bool sharedBlend) +{ + if (!sharedBlend) + return Vector4(color.red, color.green, color.blue, alpha); + + if (shaderType == ParticleSystemInfo::ADDITIVE) + return Vector4(color.red, color.green, color.blue, 0.0f); + + alpha = clamp(0.0f, alpha, 1.0f); + return Vector4(clamp(0.0f, color.red, 1.0f) * alpha, clamp(0.0f, color.green, 1.0f) * alpha, clamp(0.0f, color.blue, 1.0f) * alpha, alpha); +} W3DParticleSystemManager::W3DParticleSystemManager() { m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD; - m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; m_pointGroup = nullptr; m_streakLine = nullptr; @@ -92,6 +138,12 @@ W3DParticleSystemManager::~W3DParticleSystemManager() REF_PTR_RELEASE(m_angleBuffer); } +void W3DParticleSystemManager::reset() +{ + ParticleSystemManager::reset(); + m_atlas.Reset(); +} + /** * Hack because DoParticles is called from Flush(), which is called * multiple times per frame. We only want to render once. @@ -219,8 +271,24 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) RefCountPtr texture; texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); + // TheSuperHackers @performance stephanmeesters 18/10/2026 Draw sprites from a shared atlas page where possible, + // so particles with different textures, and alpha and additive particles, can join the same batch. + const ParticleSystemInfo::ParticleShaderType shaderType = sys->getShaderType(); + Vector4 uvRect(0.0f, 0.0f, 1.0f, 1.0f); + Bool atlased = FALSE; + if (supportsAtlasing(*sys)) + { + if (TextureClass *page = m_atlas.Find_Or_Insert(texture.Peek(), shaderType == ParticleSystemInfo::ALPHA, uvRect)) + { + texture = page; + atlased = TRUE; + } + } + const Bool sharedBlend = usesSharedBlend(shaderType, atlased); + const ShaderClass shader = getParticleShader(shaderType, sharedBlend); + const Bool canBatch = sys->isUsingParticles(); - const Bool batchDone = finishedBatch(*sys, texture); + const Bool batchDone = finishedBatch(*sys, texture, shader); if (!canBatch || batchDone) { flushParticleBatch(rinfo, pointCount); @@ -229,7 +297,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // setup a new particle batch texture if prior batch was flushed. if (canBatch && m_batchTexture == nullptr) { - initializeBatch(*sys, texture); + initializeBatch(*sys, texture, shader); } UnsignedInt startCount = pointCount; @@ -266,11 +334,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) sizeArray[pointCount] = psize; color = p->getColor(); - RGBAArray[pointCount].X = color->red; - RGBAArray[pointCount].Y = color->green; - RGBAArray[pointCount].Z = color->blue; - RGBAArray[pointCount].W = p->getAlpha(); + RGBAArray[pointCount] = getVertexColor(*color, p->getAlpha(), shaderType, sharedBlend); + m_uvRects[pointCount] = uvRect; angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); if (++pointCount == MAX_POINTS_PER_GROUP) @@ -284,7 +350,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. m_onScreenParticleCount += (pointCount - startCount); flushParticleBatch(rinfo, pointCount); - initializeBatch(*sys, texture); + initializeBatch(*sys, texture, shader); startCount = 0; } } @@ -345,7 +411,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) else if (sys->isUsingStreak() && (pointCount == 1)) { m_onScreenParticleCount += (pointCount - startCount); - initializeBatch(*sys, texture); + initializeBatch(*sys, texture, shader); flushParticleBatch(rinfo, pointCount); startCount = 0; } @@ -427,17 +493,17 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } -Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr& texture) +Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr& texture, const ShaderClass& shader) { return texture.Peek() != m_batchTexture.Peek() || - system.getShaderType() != m_batchShaderType || + shader.Get_Bits() != m_batchShader.Get_Bits() || system.getParticleAlignment() != m_batchParticleAlignment; } -void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture) +void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture, const ShaderClass& shader) { m_batchTexture = texture; - m_batchShaderType = system.getShaderType(); + m_batchShader = shader; m_batchParticleAlignment = system.getParticleAlignment(); } @@ -445,27 +511,10 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign { if (pointCount > 0) { - ShaderClass shader; - switch (m_batchShaderType) - { - case ParticleSystemInfo::ADDITIVE: - shader = ShaderClass::_PresetAdditiveSpriteShader; - break; - case ParticleSystemInfo::ALPHA: - shader = ShaderClass::_PresetAlphaSpriteShader; - break; - case ParticleSystemInfo::ALPHA_TEST: - shader = ShaderClass::_PresetATestSpriteShader; - break; - case ParticleSystemInfo::MULTIPLY: - shader = ShaderClass::_PresetMultiplicativeSpriteShader; - break; - } - if (m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING) { m_terrainParticles->setTexture(m_batchTexture.Peek()); - m_terrainParticles->setShader( shader ); + m_terrainParticles->setShader( m_batchShader ); m_terrainParticles->setArrays( m_posBuffer, m_RGBABuffer, m_sizeBuffer, m_angleBuffer, pointCount ); m_terrainParticles->setBoundingBox( m_currentBoundingBox ); m_terrainParticles->render(); @@ -473,12 +522,13 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign else // draw regular point group { m_pointGroup->Set_Texture(m_batchTexture.Peek()); - m_pointGroup->Set_Shader(shader); + m_pointGroup->Set_Shader(m_batchShader); m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); m_pointGroup->Set_Billboard(m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD); m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Set_Texture_UV_Rects(m_uvRects); m_pointGroup->Render(rinfo); } @@ -487,5 +537,5 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign m_batchTexture.Clear(); m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD; - m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + m_batchShader = ShaderClass(); } diff --git a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt index 0bc089fa5f0..d376769ec9a 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt @@ -208,6 +208,8 @@ set(WW3D2_SRC textdraw.h texture.cpp texture.h + textureatlas.cpp + textureatlas.h texturefilter.cpp texturefilter.h textureloader.cpp diff --git a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp index 58b9dfaf712..c418a161c0c 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp @@ -156,6 +156,7 @@ PointGroupClass::PointGroupClass() : PointOrientation(nullptr), PointFrame(nullptr), PointCount(0), + TextureUVRects(nullptr), FrameRowColumnCountLog2(0), Texture(nullptr), Flags(0), @@ -915,6 +916,17 @@ void PointGroupClass::Render(RenderInfoClass &rinfo) Update_Arrays(current_loc, current_diffuse, current_size, current_orient, current_frame, PointCount, PointLoc->Get_Count(), vnum, pnum); + // Map each point's frame UVs into its rectangle; Update_Arrays refills VertexUV every render. + if (TextureUVRects) { + const int verts_per_point = (PointMode == QUADS) ? 4 : 3; + for (int point = 0, vert = 0; point < PointCount; ++point) { + const Vector4& rect = TextureUVRects[point]; + for (const int end = vert + verts_per_point; vert < end; ++vert) { + VertexUV[vert].Set(rect.X + VertexUV[vert].X * (rect.Z - rect.X), rect.Y + VertexUV[vert].Y * (rect.W - rect.Y)); + } + } + } + // the locations are now in view space // so set world and view matrices to identity and render diff --git a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.h b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.h index 5b1a23d1413..fc077e46397 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.h @@ -86,6 +86,7 @@ class PointGroupClass int active_point_count = -1, float vpxmin = 0.0f, float vpymin = 0.0f, float vpxmax = 0.0f, float vpymax = 0.0f); + void Set_Texture_UV_Rects(const Vector4* rects) { TextureUVRects = rects; } void Set_Point_Size(float size); float Get_Point_Size(); void Set_Point_Color(Vector3 color); @@ -158,6 +159,7 @@ class PointGroupClass ShareBufferClass * PointFrame; // (null if not used) frame indices int PointCount; // Active (if APT) or total point count + const Vector4 * TextureUVRects; // (null if not used) uv rectangle per point // See comments for Get/Set_Frame_Row_Column_Count_Log2 above unsigned char FrameRowColumnCountLog2; // MUST be equal or lesser than 4 diff --git a/Core/Libraries/Source/WWVegas/WW3D2/sortingrenderer.cpp b/Core/Libraries/Source/WWVegas/WW3D2/sortingrenderer.cpp index 4ae57efd214..4fefe21a086 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/sortingrenderer.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/sortingrenderer.cpp @@ -362,6 +362,19 @@ void SortingRendererClass::Insert_To_Sorting_Pool(SortingNodeStruct* state) // ---------------------------------------------------------------------------- //static unsigned prevLight = 0xffffffff; +static bool Same_Render_State(const RenderStateStruct& a, const RenderStateStruct& b) +{ + if (a.shader.Get_Bits()!=b.shader.Get_Bits() || a.material!=b.material || a.material->Get_Lighting()) + return false; + + for (int i=0;iGet_Max_Textures_Per_Pass();++i) { + if (a.Textures[i]!=b.Textures[i]) + return false; + } + + return memcmp(&a.world, &b.world, sizeof(a.world))==0 && memcmp(&a.view, &b.view, sizeof(a.view))==0; +} + static void Apply_Render_State(RenderStateStruct& render_state) { DX8Wrapper::Set_Shader(render_state.shader); @@ -546,37 +559,23 @@ void SortingRendererClass::Flush_Sorting_Pool() DX8Wrapper::Apply_Render_State_Changes(); - unsigned count_to_render=1; - unsigned start_index=0; - unsigned node_id=tis[chunkOffset].idx; - for (unsigned i=chunkOffset + 1;isorting_state); - - DX8Wrapper::Draw_Triangles( - start_index*3, - count_to_render, - state->min_vertex_index, - state->vertex_count); - - count_to_render=0; - start_index=i - chunkOffset; - node_id=tis[i].idx; + // TheSuperHackers @performance stephanmeesters 18/10/2026 Draw polygons that in the same call when they share the same render state. + for (unsigned start=chunkOffset;startmin_vertex_index; + unsigned end_vertex=min_vertex + state->vertex_count; + unsigned end=start + 1; + for (;endsorting_state, node->sorting_state)) + break; + min_vertex=std::min(min_vertex, node->min_vertex_index); + end_vertex=std::max(end_vertex, node->min_vertex_index + node->vertex_count); } - count_to_render++; //keep track of number of polygons of same kind - } - // Render any remaining polygons... - if (count_to_render) { - SortingNodeStruct* state=overlapping_nodes[node_id]; Apply_Render_State(state->sorting_state); - - DX8Wrapper::Draw_Triangles( - start_index*3, - count_to_render, - state->min_vertex_index, - state->vertex_count); + DX8Wrapper::Draw_Triangles((start - chunkOffset)*3, end - start, min_vertex, end_vertex - min_vertex); + start=end; } chunkOffset += chunkCount; diff --git a/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp b/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp new file mode 100644 index 00000000000..d5294eb9287 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp @@ -0,0 +1,285 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "WW3D2/textureatlas.h" +#include "WW3D2/surfaceclass.h" +#include "WW3D2/textureloader.h" +#include "WWMath/vector2i.h" +#include "WWMath/wwmath.h" +#include "WW3D2/ww3d.h" +#include "Utility/premultiply_alpha.h" +#include +#include + +void TextureAtlasClass::Reset() +{ + TextureLocations[false].clear(); + TextureLocations[true].clear(); + Pages.clear(); +} + +TextureClass* TextureAtlasClass::Find_Or_Insert(TextureClass* source, bool premultiply, Vector4& rect) +{ + if (source == nullptr) + { + return nullptr; + } + + TextureLocationMap& locations = TextureLocations[premultiply]; + TextureLocationMap::iterator it = locations.find(source->Get_Texture_Name()); + if (it == locations.end()) + { + // To prevent lag spikes, limit the number of textures that can be inserted (packed) per frame. + if (!Has_Pack_Budget()) + { + return nullptr; + } + + if (!source->Is_Initialized()) + { + TextureLoader::Request_Foreground_Loading(source); + } + + TextureLocation packedTexture = Add_Texture(source, premultiply); + it = locations.insert(std::make_pair(source->Get_Texture_Name(), packedTexture)).first; + ++PacksThisFrame; + } + + // Packing failed: unsupported size, no room left, or a page could not be created or written. + if (it->second.pageIndex == TextureAtlas::NO_PAGE) + { + return nullptr; + } + + rect = it->second.uvRect; + return Pages[it->second.pageIndex].texture.Peek(); +} + +TextureAtlasClass::TextureLocation TextureAtlasClass::Add_Texture(TextureClass* source, bool premultiply) +{ + TextureLocation location; + location.pageIndex = TextureAtlas::NO_PAGE; + + const int width = source->Get_Width(); + const int height = source->Get_Height(); + const int cellWidth = width + 2 * TextureAtlas::PADDING; + const int cellHeight = height + 2 * TextureAtlas::PADDING; + + // Check size constraints. + const bool powerOfTwo = WWMath::Is_Power_Of_2(width) && WWMath::Is_Power_Of_2(height); + const bool largeEnough = width >= TextureAtlas::PADDING && height >= TextureAtlas::PADDING; + const bool fitsOnPage = cellWidth <= TextureAtlas::PAGE_SIZE && cellHeight <= TextureAtlas::PAGE_SIZE; + if (!powerOfTwo || !largeEnough || !fitsOnPage) + { + return location; + } + + // Reserve space in the atlas, then write the new texture to it. + int pageIndex = 0; + int x = 0; + int y = 0; + if (!Allocate_Cell(cellWidth, cellHeight, pageIndex, x, y) || + !Copy_To_Cell(source, premultiply, Pages[pageIndex].texture.Peek(), x + TextureAtlas::PADDING, y + TextureAtlas::PADDING)) + { + return location; + } + + constexpr const float scale = 1.0f / TextureAtlas::PAGE_SIZE; + location.pageIndex = pageIndex; + location.uvRect.Set((x + TextureAtlas::PADDING) * scale, + (y + TextureAtlas::PADDING) * scale, + (x + TextureAtlas::PADDING + width) * scale, + (y + TextureAtlas::PADDING + height) * scale); + return location; +} + +bool TextureAtlasClass::Allocate_Cell(int width, int height, int& pageIndex, int& x, int& y) +{ + // Use the first row of exactly this height that still has room. + Row* row = nullptr; + for (int p = 0; p < Pages.size() && row == nullptr; ++p) + { + for (int r = 0; r < Pages[p].rows.size() && row == nullptr; ++r) + { + Row& candidate = Pages[p].rows[r]; + if (candidate.height == height && candidate.usedWidth + width <= TextureAtlas::PAGE_SIZE) + { + row = &candidate; + pageIndex = p; + } + } + } + + if (row == nullptr) + { + // Otherwise find the first page with enough height left, creating a new page if none has. + int p = 0; + while (p < Pages.size() && Pages[p].usedHeight + height > TextureAtlas::PAGE_SIZE) + { + ++p; + } + if (p == Pages.size() && !Create_Page()) + { + return false; + } + + // Start a new row below the page's existing rows. + Page& page = Pages[p]; + Row added = { page.usedHeight, height, 0 }; + page.rows.push_back(added); + page.usedHeight += height; + row = &page.rows.back(); + pageIndex = p; + } + + // Take the next free spot in the row, left to right. + x = row->usedWidth; + y = row->y; + row->usedWidth += width; + return true; +} + +bool TextureAtlasClass::Create_Page() +{ + if (Pages.size() >= TextureAtlas::MAX_PAGES) + { + return false; + } + + Page page; + page.texture.Assign_No_Add_Ref(NEW_REF(TextureClass, (TextureAtlas::PAGE_SIZE, TextureAtlas::PAGE_SIZE, WW3D_FORMAT_A8R8G8B8, (MipCountType)TextureAtlas::MIP_LEVEL_COUNT))); + page.usedHeight = 0; + + // The device may give a different format, size or mip count than asked for. In that case a page cannot be created. + if (page.texture->Peek_D3D_Texture() == nullptr || page.texture->Peek_D3D_Texture()->GetLevelCount() != TextureAtlas::MIP_LEVEL_COUNT) + { + return false; + } + + SurfaceClass::SurfaceDescription desc; + page.texture->Get_Level_Description(desc, 0); + if (desc.Format != WW3D_FORMAT_A8R8G8B8 || desc.Width != TextureAtlas::PAGE_SIZE || desc.Height != TextureAtlas::PAGE_SIZE) + { + return false; + } + + // Bilinear or trilinear filtering only: anisotropic filtering samples further than the padding reaches. + // Clamp addressing, so samples at the page edge never wrap around to the opposite side. + TextureFilterClass& filter = page.texture->Get_Filter(); + filter.Set_Min_Filter(TextureFilterClass::FILTER_TYPE_FAST); + filter.Set_Mag_Filter(TextureFilterClass::FILTER_TYPE_FAST); + filter.Set_Mip_Mapping(TextureFilterClass::FILTER_TYPE_DEFAULT); + filter.Set_U_Addr_Mode(TextureFilterClass::TEXTURE_ADDRESS_CLAMP); + filter.Set_V_Addr_Mode(TextureFilterClass::TEXTURE_ADDRESS_CLAMP); + + Pages.push_back(page); + return true; +} + +bool TextureAtlasClass::Copy_To_Cell(TextureClass* source, bool premultiply, TextureClass* page, int left, int top) +{ + const int sourceLevels = (int)source->Peek_D3D_Texture()->GetLevelCount(); + + // WW3D stores the address mode on the texture and applies it whenever the texture is drawn. + // Padding will use that same filtering as in normal rendering. + const bool wrapU = source->Get_Filter().Get_U_Addr_Mode() == TextureFilterClass::TEXTURE_ADDRESS_REPEAT; + const bool wrapV = source->Get_Filter().Get_V_Addr_Mode() == TextureFilterClass::TEXTURE_ADDRESS_REPEAT; + + for (int level = 0; level < TextureAtlas::MIP_LEVEL_COUNT; ++level) + { + const int padding = TextureAtlas::PADDING >> level; + const int width = source->Get_Width() >> level; + const int height = source->Get_Height() >> level; + const int x = left >> level; + const int y = top >> level; + + // If the source has this mip level, copy it, converting to A8R8G8B8. Using the shipped mips keeps sprites looking + // as they did. If the source has fewer levels than the page, scale down this cell's level above instead. + RefCountPtr destinationSurface; + destinationSurface.Assign_No_Add_Ref(page->Get_Surface_Level(level)); + RefCountPtr sourceSurface; + if (level < sourceLevels) + { + sourceSurface.Assign_No_Add_Ref(source->Get_Surface_Level(level)); + destinationSurface->Copy(x, y, 0, 0, width, height, sourceSurface.Peek()); + } + else + { + sourceSurface.Assign_No_Add_Ref(page->Get_Surface_Level(level - 1)); + destinationSurface->Stretch_Copy(x, y, width, height, x * 2, y * 2, width * 2, height * 2, sourceSurface.Peek()); + } + + // Lock the cell, padding included. Locking the top level also marks the cell for upload at every level. + int pitch = 0; + unsigned char* cell = (unsigned char*)destinationSurface->Lock(&pitch, Vector2i(x - padding, y - padding), Vector2i(x + width + padding, y + height + padding)); + if (cell == nullptr) + { + return false; + } + + // Premultiply the copied texels. Filtered levels come from the level above, which is already premultiplied. + if (level < sourceLevels && premultiply) + { + premultiply_alpha(cell + padding * pitch + padding * sizeof(unsigned), pitch, width, height); + } + + // Fill the padding from the texture's edges. + Fill_Padding(cell, pitch, width, height, padding, wrapU, wrapV); + destinationSurface->Unlock(); + } + return true; +} + +bool TextureAtlasClass::Has_Pack_Budget() +{ + if (PackFrame != WW3D::Get_Frame_Count()) + { + PackFrame = WW3D::Get_Frame_Count(); + PacksThisFrame = 0; + } + return PacksThisFrame < TextureAtlas::MAX_PACKS_PER_FRAME; +} + +int TextureAtlasClass::Wrap_Or_Clamp(int coordinate, int size, bool wrap) +{ + if (wrap) + { + return coordinate & (size - 1); + } + return WWMath::Clamp_Int(coordinate, 0, size - 1); +} + +void TextureAtlasClass::Fill_Padding(unsigned char* cell, int pitch, int width, int height, int padding, bool wrapU, bool wrapV) +{ + for (int y = padding; y < padding + height; ++y) + { + unsigned* line = (unsigned*)(cell + y * pitch); + for (int x = 0; x < padding; ++x) + { + line[x] = line[padding + Wrap_Or_Clamp(x - padding, width, wrapU)]; + line[padding + width + x] = line[padding + Wrap_Or_Clamp(width + x, width, wrapU)]; + } + } + + const int lineBytes = (width + 2 * padding) * sizeof(unsigned); + for (int row = 0; row < padding; ++row) + { + memcpy(cell + row * pitch, cell + (padding + Wrap_Or_Clamp(row - padding, height, wrapV)) * pitch, lineBytes); + memcpy(cell + (padding + height + row) * pitch, cell + (padding + Wrap_Or_Clamp(height + row, height, wrapV)) * pitch, lineBytes); + } +} diff --git a/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h b/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h new file mode 100644 index 00000000000..ae5d9459501 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h @@ -0,0 +1,88 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "WW3D2/texture.h" +#include "WWLib/ref_ptr.h" +#include "WWLib/wwstring.h" +#include "WWMath/vector4.h" +#include +#include + +namespace TextureAtlas +{ +constexpr const int PAGE_SIZE = 2048; +constexpr const int MIP_LEVEL_COUNT = 5; +constexpr const int PADDING = 1 << (MIP_LEVEL_COUNT - 1); +constexpr const int MAX_PAGES = 4; +constexpr const int MAX_PACKS_PER_FRAME = 2; // Spreads a burst of new textures over several frames. + +constexpr const int NO_PAGE = -1; // Page index of a texture that cannot be packed. +} // namespace TextureAtlas + +/// Packs textures into shared atlas pages. +/// Page: a 2048x2048 texture with 5 mip levels, holding many cells. +/// Cell: one texture plus its padding, a border of repeated edge texels that keeps filtering from reaching a neighbour. +/// Row: a strip across a page holding cells of the same height, left to right. +/// Premultiply: optionally multiplies a cell's color by its alpha. +class TextureAtlasClass +{ +public: + TextureAtlasClass() : PackFrame(0), PacksThisFrame(0) {} + + TextureClass* Find_Or_Insert(TextureClass* source, bool premultiply, Vector4& rect); // Gets the page holding the texture and its uv rectangle, packing the texture first if needed. + void Reset(); // Release all pages. + + static int Wrap_Or_Clamp(int coordinate, int size, bool wrap); + static void Fill_Padding(unsigned char* cell, int pitch, int width, int height, int padding, bool wrapU, bool wrapV); + +private: + struct Row + { + int y; + int height; + int usedWidth; + }; + + struct Page + { + RefCountPtr texture; + std::vector rows; + int usedHeight; + }; + + struct TextureLocation + { + int pageIndex; // NO_PAGE when the texture cannot be packed + Vector4 uvRect; + }; + + typedef std::map TextureLocationMap; + + bool Has_Pack_Budget(); + TextureLocation Add_Texture(TextureClass* source, bool premultiply); + bool Allocate_Cell(int width, int height, int& pageIndex, int& x, int& y); + bool Create_Page(); + static bool Copy_To_Cell(TextureClass* source, bool premultiply, TextureClass* page, int left, int top); + + std::vector Pages; + TextureLocationMap TextureLocations[2]; // Indexed by premultiply. + unsigned PackFrame; + int PacksThisFrame; +}; diff --git a/Dependencies/Utility/CMakeLists.txt b/Dependencies/Utility/CMakeLists.txt index b49fb225373..c8c1e92ef31 100644 --- a/Dependencies/Utility/CMakeLists.txt +++ b/Dependencies/Utility/CMakeLists.txt @@ -17,6 +17,10 @@ set(UTILITY_SRC Utility/iostream_adapter.h Utility/lazy_static.h Utility/mem_compat.h + Utility/premultiply_alpha.h + Utility/premultiply_alpha.inl + Utility/premultiply_alpha_sse2.inl + Utility/premultiply_alpha_x86asm.inl Utility/pseh_compat.h Utility/sstream_adapter.h Utility/stdio_adapter.h diff --git a/Dependencies/Utility/Utility/premultiply_alpha.h b/Dependencies/Utility/Utility/premultiply_alpha.h new file mode 100644 index 00000000000..88cba457a86 --- /dev/null +++ b/Dependencies/Utility/Utility/premultiply_alpha.h @@ -0,0 +1,34 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +// Multiplies the color of each A8R8G8B8 pixel by its alpha, rounding to nearest. Alpha is kept. +void premultiply_alpha(unsigned char* pixels, int pitch, int width, int height); + +// Premultiply alpha. Multiple implementations: 1) VC6-compatible in x86 ASM; 2) SSE2 intrinsics; 3) portable. +#if defined(_MSC_VER) && _MSC_VER < 1300 + #include "premultiply_alpha_x86asm.inl" +#else + #if defined(__SSE2__) || defined(_M_X64) || (defined(_MSC_VER) && defined(_M_IX86)) + #include + #include "premultiply_alpha_sse2.inl" + #else + #include "premultiply_alpha.inl" + #endif +#endif diff --git a/Dependencies/Utility/Utility/premultiply_alpha.inl b/Dependencies/Utility/Utility/premultiply_alpha.inl new file mode 100644 index 00000000000..196e6139743 --- /dev/null +++ b/Dependencies/Utility/Utility/premultiply_alpha.inl @@ -0,0 +1,36 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// Portable implementation of premultiply_alpha. +inline void premultiply_alpha(unsigned char* pixels, int pitch, int width, int height) +{ + for (int y = 0; y < height; ++y, pixels += pitch) + { + unsigned int* line = reinterpret_cast(pixels); + for (int x = 0; x < width; ++x) + { + const unsigned int alpha = line[x] >> 24; + unsigned int result = line[x] & 0xFF000000u; + for (unsigned int shift = 0; shift < 24; shift += 8) + { + result |= ((((line[x] >> shift) & 255) * alpha + 127) / 255) << shift; + } + line[x] = result; + } + } +} diff --git a/Dependencies/Utility/Utility/premultiply_alpha_sse2.inl b/Dependencies/Utility/Utility/premultiply_alpha_sse2.inl new file mode 100644 index 00000000000..e03deda1543 --- /dev/null +++ b/Dependencies/Utility/Utility/premultiply_alpha_sse2.inl @@ -0,0 +1,65 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// SSE2 implementation of premultiply_alpha. +// Adapted from v1 of https://github.com/Wizermil/premultiply_alpha (MIT License, Copyright (c) 2019 Mathieu Garaud). +inline void premultiply_alpha(unsigned char* pixels, int pitch, int width, int height) +{ + const __m128i alphaMask = _mm_set1_epi32(static_cast(0xFF000000u)); + const __m128i oddMask = _mm_set1_epi16(static_cast(0xFF00)); + const __m128i half = _mm_set1_epi16(127); + const __m128i div255 = _mm_set1_epi16(static_cast(0x8081)); + for (int y = 0; y < height; ++y, pixels += pitch) + { + unsigned int* line = reinterpret_cast(pixels); + int x = 0; + for (; x + 4 <= width; x += 4) + { + __m128i* const p = reinterpret_cast<__m128i*>(line + x); + const __m128i color = _mm_loadu_si128(p); + + // Alpha times 256 in both 16-bit halves of each pixel. + __m128i alpha = _mm_and_si128(color, alphaMask); + alpha = _mm_or_si128(alpha, _mm_srli_epi32(alpha, 16)); + + // Blue and red sit in the even bytes, green and alpha in the odd bytes. Moving each to the top of its 16-bit lane + // makes the high half of the product with alpha * 256 equal to c * a. + __m128i even = _mm_mulhi_epu16(_mm_slli_epi16(color, 8), alpha); + __m128i odd = _mm_mulhi_epu16(_mm_and_si128(color, oddMask), alpha); + + // (c * a + 127) / 255, as (v * 0x8081) >> 23, which is exact for every 16-bit v. + even = _mm_srli_epi16(_mm_mulhi_epu16(_mm_add_epi16(even, half), div255), 7); + odd = _mm_srli_epi16(_mm_mulhi_epu16(_mm_add_epi16(odd, half), div255), 7); + + const __m128i result = _mm_or_si128(even, _mm_slli_epi16(odd, 8)); + _mm_storeu_si128(p, _mm_or_si128(_mm_andnot_si128(alphaMask, result), _mm_and_si128(alphaMask, color))); + } + + // The last 0 to 3 pixels of the line. + for (; x < width; ++x) + { + const unsigned int alpha = line[x] >> 24; + unsigned int result = line[x] & 0xFF000000u; + for (unsigned int shift = 0; shift < 24; shift += 8) + { + result |= ((((line[x] >> shift) & 255) * alpha + 127) / 255) << shift; + } + line[x] = result; + } + } +} diff --git a/Dependencies/Utility/Utility/premultiply_alpha_x86asm.inl b/Dependencies/Utility/Utility/premultiply_alpha_x86asm.inl new file mode 100644 index 00000000000..d2bfcf4183e --- /dev/null +++ b/Dependencies/Utility/Utility/premultiply_alpha_x86asm.inl @@ -0,0 +1,231 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// VC6-compatible x86 implementation of premultiply_alpha. +// An SSE2-capable CPU is assumed. +inline void premultiply_alpha(unsigned char* pixels, int pitch, int width, int height) +{ + for (int y = 0; y < height; ++y, pixels += pitch) + { + unsigned int* line = reinterpret_cast(pixels); + const int groups = width / 4; + if (groups != 0) + { + // clang-format off + __asm { + mov esi, line + mov ecx, groups + mov eax, 0xFF000000 + // movd xmm7, eax + _emit 0x66 + _emit 0x0f + _emit 0x6e + _emit 0xf8 + // pshufd xmm7, xmm7, 0 (alpha byte mask) + _emit 0x66 + _emit 0x0f + _emit 0x70 + _emit 0xff + _emit 0x00 + mov eax, 0xFF00FF00 + // movd xmm6, eax + _emit 0x66 + _emit 0x0f + _emit 0x6e + _emit 0xf0 + // pshufd xmm6, xmm6, 0 (odd byte mask) + _emit 0x66 + _emit 0x0f + _emit 0x70 + _emit 0xf6 + _emit 0x00 + mov eax, 0x007F007F + // movd xmm5, eax + _emit 0x66 + _emit 0x0f + _emit 0x6e + _emit 0xe8 + // pshufd xmm5, xmm5, 0 (127 in each 16-bit lane) + _emit 0x66 + _emit 0x0f + _emit 0x70 + _emit 0xed + _emit 0x00 + mov eax, 0x80818081 + // movd xmm4, eax + _emit 0x66 + _emit 0x0f + _emit 0x6e + _emit 0xe0 + // pshufd xmm4, xmm4, 0 (0x8081 in each 16-bit lane) + _emit 0x66 + _emit 0x0f + _emit 0x70 + _emit 0xe4 + _emit 0x00 + next_group: + // movdqu xmm0, [esi] (color) + _emit 0xf3 + _emit 0x0f + _emit 0x6f + _emit 0x06 + // movdqa xmm1, xmm0 + _emit 0x66 + _emit 0x0f + _emit 0x6f + _emit 0xc8 + // pand xmm1, xmm7 + _emit 0x66 + _emit 0x0f + _emit 0xdb + _emit 0xcf + // movdqa xmm2, xmm1 + _emit 0x66 + _emit 0x0f + _emit 0x6f + _emit 0xd1 + // psrld xmm2, 16 (alpha times 256 in both 16-bit halves) + _emit 0x66 + _emit 0x0f + _emit 0x72 + _emit 0xd2 + _emit 0x10 + // por xmm1, xmm2 + _emit 0x66 + _emit 0x0f + _emit 0xeb + _emit 0xca + // movdqa xmm2, xmm0 (c * a for blue and red) + _emit 0x66 + _emit 0x0f + _emit 0x6f + _emit 0xd0 + // psllw xmm2, 8 + _emit 0x66 + _emit 0x0f + _emit 0x71 + _emit 0xf2 + _emit 0x08 + // pmulhuw xmm2, xmm1 + _emit 0x66 + _emit 0x0f + _emit 0xe4 + _emit 0xd1 + // movdqa xmm3, xmm0 (c * a for green and alpha) + _emit 0x66 + _emit 0x0f + _emit 0x6f + _emit 0xd8 + // pand xmm3, xmm6 + _emit 0x66 + _emit 0x0f + _emit 0xdb + _emit 0xde + // pmulhuw xmm3, xmm1 + _emit 0x66 + _emit 0x0f + _emit 0xe4 + _emit 0xd9 + // paddw xmm2, xmm5 ((v + 127) / 255) + _emit 0x66 + _emit 0x0f + _emit 0xfd + _emit 0xd5 + // pmulhuw xmm2, xmm4 + _emit 0x66 + _emit 0x0f + _emit 0xe4 + _emit 0xd4 + // psrlw xmm2, 7 + _emit 0x66 + _emit 0x0f + _emit 0x71 + _emit 0xd2 + _emit 0x07 + // paddw xmm3, xmm5 ((v + 127) / 255) + _emit 0x66 + _emit 0x0f + _emit 0xfd + _emit 0xdd + // pmulhuw xmm3, xmm4 + _emit 0x66 + _emit 0x0f + _emit 0xe4 + _emit 0xdc + // psrlw xmm3, 7 (result) + _emit 0x66 + _emit 0x0f + _emit 0x71 + _emit 0xd3 + _emit 0x07 + // psllw xmm3, 8 + _emit 0x66 + _emit 0x0f + _emit 0x71 + _emit 0xf3 + _emit 0x08 + // por xmm2, xmm3 + _emit 0x66 + _emit 0x0f + _emit 0xeb + _emit 0xd3 + // movdqa xmm1, xmm7 (keep the original alpha) + _emit 0x66 + _emit 0x0f + _emit 0x6f + _emit 0xcf + // pandn xmm1, xmm2 + _emit 0x66 + _emit 0x0f + _emit 0xdf + _emit 0xca + // pand xmm0, xmm7 + _emit 0x66 + _emit 0x0f + _emit 0xdb + _emit 0xc7 + // por xmm0, xmm1 + _emit 0x66 + _emit 0x0f + _emit 0xeb + _emit 0xc1 + // movdqu [esi], xmm0 + _emit 0xf3 + _emit 0x0f + _emit 0x7f + _emit 0x06 + add esi, 16 + dec ecx + jnz next_group + } + // clang-format on + } + + // The last 0 to 3 pixels of the line. + for (int x = groups * 4; x < width; ++x) + { + const unsigned int alpha = line[x] >> 24; + unsigned int result = line[x] & 0xFF000000u; + for (unsigned int shift = 0; shift < 24; shift += 8) + { + result |= ((((line[x] >> shift) & 255) * alpha + 127) / 255) << shift; + } + line[x] = result; + } + } +} diff --git a/Tests/Google/Core/CMakeLists.txt b/Tests/Google/Core/CMakeLists.txt index 03efb5a3ac9..b91b1874b89 100644 --- a/Tests/Google/Core/CMakeLists.txt +++ b/Tests/Google/Core/CMakeLists.txt @@ -5,6 +5,7 @@ set(COREI_GOOGLETEST_SRC GameEngine/Common/INITest.cpp GameEngineDevice/StdDevice/Common/StdLocalFileSystemTest.cpp Libraries/Compression/CompressionManagerTest.cpp + Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp Libraries/WWVegas/WWMath/WWMathCalls.h Libraries/WWVegas/WWMath/WWMathDumps.h Libraries/WWVegas/WWMath/WWMathKeys.h diff --git a/Tests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp b/Tests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp new file mode 100644 index 00000000000..a55fd8cd690 --- /dev/null +++ b/Tests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp @@ -0,0 +1,68 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include +#include + +#include "WW3D2/textureatlas.h" + +namespace +{ +// A cell of 2x2 texels with 1 texel of padding: 4x4 pixels, interior values 1 2 / 3 4. +std::vector makeCell() +{ + std::vector cell(16, 0); + cell[1 * 4 + 1] = 1; + cell[1 * 4 + 2] = 2; + cell[2 * 4 + 1] = 3; + cell[2 * 4 + 2] = 4; + return cell; +} + +const int CELL_PITCH = 4 * sizeof(unsigned); +} // namespace + +TEST(TextureAtlas, WrapOrClampClampsToNearestEdge) +{ + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(-1, 8, false), 0); + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(3, 8, false), 3); + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(8, 8, false), 7); +} + +TEST(TextureAtlas, WrapOrClampWrapsToOppositeEdge) +{ + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(-1, 8, true), 7); + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(8, 8, true), 0); + EXPECT_EQ(TextureAtlasClass::Wrap_Or_Clamp(-9, 8, true), 7); +} + +TEST(TextureAtlas, FillPaddingRepeatsNearestEdgeWhenClamping) +{ + std::vector cell = makeCell(); + TextureAtlasClass::Fill_Padding((unsigned char*)cell.data(), CELL_PITCH, 2, 2, 1, false, false); + const unsigned expected[16] = { 1, 1, 2, 2, 1, 1, 2, 2, 3, 3, 4, 4, 3, 3, 4, 4 }; + EXPECT_EQ(cell, std::vector(expected, expected + 16)); +} + +TEST(TextureAtlas, FillPaddingRepeatsOppositeEdgeWhenWrapping) +{ + std::vector cell = makeCell(); + TextureAtlasClass::Fill_Padding((unsigned char*)cell.data(), CELL_PITCH, 2, 2, 1, true, true); + const unsigned expected[16] = { 4, 3, 4, 3, 2, 1, 2, 1, 4, 3, 4, 3, 2, 1, 2, 1 }; + EXPECT_EQ(cell, std::vector(expected, expected + 16)); +} diff --git a/Tests/Google/Dependencies/CMakeLists.txt b/Tests/Google/Dependencies/CMakeLists.txt index f9e4a3354cc..77a99d8e337 100644 --- a/Tests/Google/Dependencies/CMakeLists.txt +++ b/Tests/Google/Dependencies/CMakeLists.txt @@ -3,6 +3,10 @@ set(DEPSI_GOOGLETEST_SRC Utility/atomic_x86interlocked_test.cpp Utility/interlocked_x86asm_common.h Utility/interlocked_x86asm_test.cpp + Utility/premultiply_alpha_common.h + Utility/premultiply_alpha_portable_test.cpp + Utility/premultiply_alpha_sse2_test.cpp + Utility/premultiply_alpha_x86asm_test.cpp Utility/stringex_memcchr_common.h Utility/stringex_memcchr_portable_test.cpp Utility/stringex_memcchr_sse2_test.cpp diff --git a/Tests/Google/Dependencies/Utility/premultiply_alpha_common.h b/Tests/Google/Dependencies/Utility/premultiply_alpha_common.h new file mode 100644 index 00000000000..3c6117f3086 --- /dev/null +++ b/Tests/Google/Dependencies/Utility/premultiply_alpha_common.h @@ -0,0 +1,69 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +// Checks every color and alpha pair against rounded c * a / 255. Each row holds one alpha with all 256 colors plus +// 3 fixed pixels, so the width is not a multiple of 4 and remainders run too. The rest of each row must stay untouched. +inline void testPremultiplyAlpha(void (*premultiply)(unsigned char*, Int, Int, Int)) +{ + constexpr Int width = 259; + constexpr Int stride = width + 5; + constexpr Int height = 256; + constexpr UnsignedInt untouched = 0x80FF8000u; + std::vector pixels(stride * height, untouched); + for (Int y = 0; y < height; ++y) + { + const UnsignedInt alpha = y; + UnsignedInt* row = &pixels[y * stride]; + for (UnsignedInt color = 0; color < 256; ++color) + { + row[color] = (alpha << 24) | (color << 16) | ((255 - color) << 8) | ((color * 7) & 255); + } + row[256] = 0x80FF8000u; + row[257] = 0xFF102030u; + row[258] = 0x00FFFFFFu; + } + const std::vector source = pixels; + + premultiply(reinterpret_cast(pixels.data()), stride * Int(sizeof(UnsignedInt)), width, height); + + Int mismatches = 0; + for (Int i = 0; i < stride * height; ++i) + { + UnsignedInt expected = source[i]; + if (i % stride < width) + { + const UnsignedInt alpha = source[i] >> 24; + expected = source[i] & 0xFF000000u; + for (UnsignedInt shift = 0; shift < 24; shift += 8) + { + expected |= ((((source[i] >> shift) & 255) * alpha + 127) / 255) << shift; + } + } + mismatches += pixels[i] != expected; + } + EXPECT_EQ(mismatches, 0); + EXPECT_EQ(pixels[256], 0x80804000u); + EXPECT_EQ(pixels[257], 0xFF102030u); + EXPECT_EQ(pixels[258], 0x00000000u); + EXPECT_EQ(pixels[width], untouched); +} diff --git a/Tests/Google/Dependencies/Utility/premultiply_alpha_portable_test.cpp b/Tests/Google/Dependencies/Utility/premultiply_alpha_portable_test.cpp new file mode 100644 index 00000000000..eaf2a4e7f87 --- /dev/null +++ b/Tests/Google/Dependencies/Utility/premultiply_alpha_portable_test.cpp @@ -0,0 +1,30 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "premultiply_alpha_common.h" + +// Internal linkage keeps this premultiply_alpha distinct from the other implementations linked into the test. +namespace +{ +#include "Utility/premultiply_alpha.inl" +} + +TEST(PremultiplyAlpha, Portable) +{ + testPremultiplyAlpha(premultiply_alpha); +} diff --git a/Tests/Google/Dependencies/Utility/premultiply_alpha_sse2_test.cpp b/Tests/Google/Dependencies/Utility/premultiply_alpha_sse2_test.cpp new file mode 100644 index 00000000000..28c88a9442c --- /dev/null +++ b/Tests/Google/Dependencies/Utility/premultiply_alpha_sse2_test.cpp @@ -0,0 +1,34 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "premultiply_alpha_common.h" + +#if defined(__SSE2__) || defined(_M_X64) || (defined(_MSC_VER) && defined(_M_IX86)) +#include + +// Internal linkage keeps this premultiply_alpha distinct from the other implementations linked into the test. +namespace +{ +#include "Utility/premultiply_alpha_sse2.inl" +} + +TEST(PremultiplyAlpha, Sse2) +{ + testPremultiplyAlpha(premultiply_alpha); +} +#endif diff --git a/Tests/Google/Dependencies/Utility/premultiply_alpha_x86asm_test.cpp b/Tests/Google/Dependencies/Utility/premultiply_alpha_x86asm_test.cpp new file mode 100644 index 00000000000..7c779ce792e --- /dev/null +++ b/Tests/Google/Dependencies/Utility/premultiply_alpha_x86asm_test.cpp @@ -0,0 +1,32 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "premultiply_alpha_common.h" + +#if defined(_MSC_VER) && defined(_M_IX86) +// Internal linkage keeps this premultiply_alpha distinct from the other implementations linked into the test. +namespace +{ +#include "Utility/premultiply_alpha_x86asm.inl" +} + +TEST(PremultiplyAlpha, X86Asm) +{ + testPremultiplyAlpha(premultiply_alpha); +} +#endif