diff --git a/audit/history.jsonl b/audit/history.jsonl index 9241465e..7f525d2e 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -568,3 +568,4 @@ {"date": "2026-09-28T04:59:25+00:00", "unit": "GameLib/MapOutdoorWater.cpp", "action": "unit H2: platform rewrite replaced by the full 40250 source (native-terrain gate only): far-fog second water pass restored, invented LIGHTING=FALSE save/restore and empty-texture guard removed", "evidence": "build ok; fake-server spawn frame 900 unchanged (mean abs diff ~0.4/255, no water in view); water visuals NEEDS_LIVE"} {"date": "2026-09-28T05:08:46+00:00", "unit": "EterPythonLib/PythonGraphic.cpp", "action": "unit I: SetGamma, RenderCoolTimeBox, RenderUp/DownButton, GetAvailableMemory ported from 40250 (stubs removed); cooldown fan drawn as polygon UI commands; SetViewport/RestoreViewport documented as ADAPTED (device viewport + UI clip)", "evidence": "port.app_loop drawing_primitives checks fan geometry at fTime 0.25/0.3/1.0 and viewport clip push/pop; cooldown visuals NEEDS_LIVE"} {"date": "2026-09-28T05:22:26+00:00", "unit": "UserInterface/PythonApplication.cpp", "action": "unit J: Process gains 40250's render-time/face-count block and automatic sight range (SetViewDistanceSet(0)); SetForceSightRange/SkipRenderBuffering ported from stubs; invented SetCursorPosition in UpdateGame removed; boilerplate notes on RenderGame/UpdateGame/Process/Destroy replaced (Destroy marked DIVERGENT: never called, teardown via PythonBoot::Stop)", "evidence": "ctest -j3 35/35; fake-server spawn frame 900 mean abs diff 0.33/255 vs previous (fog range not in view); sight-range visuals NEEDS_LIVE"} +{"date": "2026-09-28T05:29:23+00:00", "unit": "EterLib/GrpImageTexture.cpp", "action": "unit K: DXT DDS textures upload the file's own mip levels (40250 CreateDDSTexture: max(1,dwMipMapCount), dwLinearSize>>2i Copy) instead of a regenerated chain; non-DDS keep the D3DX_DEFAULT full chain; mem: textures one level; invented is_ui_tex no-mip rule (icon/ui/locale/<=128px) deleted; dxt_decode_test registered in ctest with mip-level cases", "evidence": "ctest -j3 36/36; fake-server spawn frame 900: near terrain diff 0.00, far terrain 2.7/255 (minified levels now from the file), rest is UI timing (area title/chat); far-terrain look NEEDS_LIVE"} diff --git a/audit/port-map/EterLib/GrpImageTexture.cpp.json b/audit/port-map/EterLib/GrpImageTexture.cpp.json new file mode 100644 index 00000000..2a54854d --- /dev/null +++ b/audit/port-map/EterLib/GrpImageTexture.cpp.json @@ -0,0 +1,60 @@ +{ + "reference": "EterLib/GrpImageTexture.cpp", + "reference_sha256": "849e6b9106cd169d97687adbcea6faba8bb0dc2ef682b51993a47443f837c9ae", + "priority": "P4", + "contracts": [], + "functions": { + "CGraphicImageTexture::Lock": { + "status": "TODO" + }, + "CGraphicImageTexture::Unlock": { + "status": "TODO" + }, + "CGraphicImageTexture::Initialize": { + "status": "TODO" + }, + "CGraphicImageTexture::Destroy": { + "status": "TODO" + }, + "CGraphicImageTexture::CreateDeviceObjects": { + "status": "TODO" + }, + "CGraphicImageTexture::Create": { + "status": "TODO" + }, + "CGraphicImageTexture::CreateFromTexturePointer": { + "status": "TODO" + }, + "CGraphicImageTexture::CreateDDSTexture": { + "status": "ADAPTED", + "impl": [ + "extension/src/dxt.cpp:decode", + "native_render/main.cpp:get_gpu_texture", + "native_render/main.cpp:upload_texture" + ], + "test": "extension/tests/dxt_decode_test.cpp", + "note": "The platform CreateDDSTexture member is a stub; its level logic lives in the texture decode/upload the renderer does. mipmapCount = max(1, min(dwMipMapCount, MAX_MIPLEVELS=12)), level i read at sum(dwLinearSize>>2k) with CDXTCImage::Copy's dwLinearSize>>2i bytes (the rest zero, uninitialised in 40250), decoded to RGBA8 (no GPU DXT) and uploaded as file levels, not regenerated. Divergence: without DDSD_MIPMAPCOUNT, or with DDSD_PITCH, 40250 creates the extra levels uninitialised; here that is one level. IsLowTextureMemory's uTexBias level skip is not applied (the port's ms_isLowTextureMemory is false). The far-terrain look NEEDS_LIVE." + }, + "CGraphicImageTexture::CreateFromMemoryFile": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/GrpImageTexture.cpp:CGraphicImageTexture::CreateFromMemoryFile", + "native_render/main.cpp:get_gpu_texture" + ], + "test": "extension/tests/dxt_decode_test.cpp", + "note": "The platform body records only the size and file name; the renderer decodes the bytes. 40250 dispatch is kept: a DXT DDS takes the CreateDDSTexture levels, anything else takes D3DXCreateTextureFromFileInMemoryEx(MipLevels=D3DX_DEFAULT), i.e. a full generated chain (vkCmdBlitImage linear, instead of D3DX's box filter). The invented is_ui_tex rule (no mips for icon/, ui/, locale/ and textures <=128px) was removed. Memory textures (mem:, 40250 CreateTexture levels=1) get one level. GRAPHICS_CAPS_HALF_SIZE_IMAGE/low-memory A4R4G4B4 re-encode is not applied." + }, + "CGraphicImageTexture::SetFileName": { + "status": "TODO" + }, + "CGraphicImageTexture::CreateFromDiskFile": { + "status": "TODO" + }, + "CGraphicImageTexture::CGraphicImageTexture": { + "status": "TODO" + }, + "CGraphicImageTexture::~CGraphicImageTexture": { + "status": "TODO" + } + } +} diff --git a/extension/CMakeLists.txt b/extension/CMakeLists.txt index 04320ebe..8743b688 100644 --- a/extension/CMakeLists.txt +++ b/extension/CMakeLists.txt @@ -86,6 +86,10 @@ if(MT_HOST_BUILD) endif() if(BUILD_TESTING AND MT_HOST_BUILD) + add_executable(dxt_decode_test tests/dxt_decode_test.cpp src/dxt.cpp) + target_include_directories(dxt_decode_test PRIVATE src) + add_test(NAME dxt.decode COMMAND $) + add_executable(net_cipher_test tests/net_cipher_test.cpp) target_link_libraries(net_cipher_test PRIVATE mtnet) add_test(NAME net.cipher_roundtrip COMMAND $) diff --git a/extension/src/dxt.cpp b/extension/src/dxt.cpp index 1e926acf..96030c5e 100644 --- a/extension/src/dxt.cpp +++ b/extension/src/dxt.cpp @@ -1,6 +1,7 @@ // engine/dxt —— DDS(DXT1/3/5) + 未压缩 BGRA8 → RGBA8 软解。M1 T3。 #include "dxt.h" #include +#include #include namespace mtgodot { @@ -81,6 +82,35 @@ uint8_t channel(uint32_t pixel, uint32_t mask) { return uint8_t((uint64_t(pixel & mask) * 255 + mask / 2) / mask); } +void decode_dxt_level(Fmt fmt, const uint8_t* src, uint32_t w, uint32_t h, uint8_t* rgba) { + const int block_bytes = (fmt == F_DXT1) ? 8 : 16; + const size_t bx = (w + 3) / 4, by = (h + 3) / 4; + for (size_t byi = 0; byi < by; ++byi) { + for (size_t bxi = 0; bxi < bx; ++bxi) { + const uint8_t* blk = src + (byi * bx + bxi) * block_bytes; + uint8_t px[16][4]; + if (fmt == F_DXT1) { + decode_color_block(blk, px, /*dxt1_alpha=*/true); + } else { + decode_color_block(blk + 8, px, /*dxt1_alpha=*/false); + if (fmt == F_DXT3) decode_dxt3_alpha(blk, px); + else decode_dxt5_alpha(blk, px); + } + for (int py = 0; py < 4; ++py) { + size_t y = byi * 4 + py; + if (y >= h) break; + for (int pxx = 0; pxx < 4; ++pxx) { + size_t x = bxi * 4 + pxx; + if (x >= w) break; + uint8_t* o = &rgba[(y * w + x) * 4]; + const uint8_t* s = px[py * 4 + pxx]; + o[0] = s[0]; o[1] = s[1]; o[2] = s[2]; o[3] = s[3]; + } + } + } + } +} + Image decode(const uint8_t* d, size_t len) { Image img; if (len < 128 || std::memcmp(d, "DDS ", 4) != 0) return img; @@ -133,31 +163,31 @@ Image decode(const uint8_t* d, size_t len) { const int block_bytes = (fmt == F_DXT1) ? 8 : 16; const size_t bx = (w + 3) / 4, by = (h + 3) / 4; if (avail < bx * by * block_bytes) { img = Image{}; return img; } + decode_dxt_level(fmt, src, uint32_t(w), uint32_t(h), img.rgba.data()); - for (size_t byi = 0; byi < by; ++byi) { - for (size_t bxi = 0; bxi < bx; ++bxi) { - const uint8_t* blk = src + (byi * bx + bxi) * block_bytes; - uint8_t px[16][4]; - if (fmt == F_DXT1) { - decode_color_block(blk, px, /*dxt1_alpha=*/true); - } else { - decode_color_block(blk + 8, px, /*dxt1_alpha=*/false); - if (fmt == F_DXT3) decode_dxt3_alpha(blk, px); - else decode_dxt5_alpha(blk, px); - } - for (int py = 0; py < 4; ++py) { - size_t y = byi * 4 + py; - if (y >= h) break; - for (int pxx = 0; pxx < 4; ++pxx) { - size_t x = bxi * 4 + pxx; - if (x >= w) break; - uint8_t* o = &img.rgba[(y * w + x) * 4]; - const uint8_t* s = px[py * 4 + pxx]; - o[0] = s[0]; o[1] = s[1]; o[2] = s[2]; o[3] = s[3]; - } - } - } + // 40250 CDXTCImage::LoadHeaderFromMemory + CGraphicImageTexture::CreateDDSTexture: + // mipmapCount = max(1, min(dwMipMapCount, MAX_MIPLEVELS)); level i starts at + // sum(dwLinearSize >> 2k) and Copy() memcpy's dwLinearSize >> 2i bytes of it. Only + // with DDSD_MIPMAPCOUNT and without DDSD_PITCH are levels > 0 filled — otherwise 40250 + // creates them uninitialized; here that case is one level. + const uint32_t dds_flags = rd_le32(d + 8); + const uint32_t linear_size = rd_le32(d + 20); + uint32_t mip_count = std::min(rd_le32(d + 28), 12u); + if (mip_count == 0) mip_count = 1; + if (!(dds_flags & 0x20000u) || (dds_flags & 0x8u) || linear_size == 0) mip_count = 1; + size_t offset = linear_size; + for (uint32_t i = 1; i < mip_count; ++i) { + const uint32_t lw = std::max(1, w >> i), lh = std::max(1, h >> i); + const size_t level_bytes = ((lw + 3) / 4) * ((lh + 3) / 4) * size_t(block_bytes); + const size_t copy_bytes = std::min(size_t(linear_size) >> (2 * i), level_bytes); + std::vector blocks(level_bytes, 0); + if (offset < avail) std::memcpy(blocks.data(), src + offset, std::min(copy_bytes, avail - offset)); + offset += size_t(linear_size) >> (2 * i); + std::vector level(size_t(lw) * lh * 4, 0); + decode_dxt_level(fmt, blocks.data(), lw, lh, level.data()); + img.mips.push_back(std::move(level)); } + img.file_mip_levels = uint8_t(mip_count); img.format = (fmt == F_DXT1) ? "DXT1" : (fmt == F_DXT3) ? "DXT3" : "DXT5"; return img; } diff --git a/extension/src/dxt.h b/extension/src/dxt.h index 81666116..6fe814d3 100644 --- a/extension/src/dxt.h +++ b/extension/src/dxt.h @@ -12,11 +12,15 @@ namespace mtgodot { struct Image { uint16_t w = 0, h = 0; std::vector rgba; // w*h*4,level 0 + // DXT DDS only: level count 40250 CreateDDSTexture gives the texture (0 = not a DXT + // DDS, caller follows the D3DX_DEFAULT full-chain path) and levels 1.. decoded to RGBA8. + uint8_t file_mip_levels = 0; + std::vector> mips; const char* format = ""; // "DXT1" / "DXT3" / "DXT5" / "BGRA8" / "" bool ok() const { return w && h && rgba.size() == size_t(w) * h * 4; } }; -// 读整个 .dds 文件字节,解 level 0 到 RGBA8。失败返回 !ok()。 +// 读整个 .dds 文件字节,解 level 0 到 RGBA8(DXT 另解文件内 mip 到 mips)。失败返回 !ok()。 Image load_dds(const uint8_t* bytes, size_t len); Image load_dds_path(const char* path); diff --git a/extension/tests/dxt_decode_test.cpp b/extension/tests/dxt_decode_test.cpp index d71e57e4..3ddf3d30 100644 --- a/extension/tests/dxt_decode_test.cpp +++ b/extension/tests/dxt_decode_test.cpp @@ -47,5 +47,23 @@ int main() { if (fourcc == 0x35545844) dxt[128] = 255; pixel(decode(dxt), 0, 255, 0, 0, 255); } - std::puts("PASS: dxt_decode_test (masked RGB, pitch, invalid headers, DXT1/3/5)"); + { // 40250 CreateDDSTexture levels: DDSD_MIPMAPCOUNT, level i at sum(linear >> 2k), + // Copy() takes linear >> 2i bytes (the 2x2 level gets 2 of its 8 block bytes). + auto mip = header(0, 0, 0, 0, 0, 32 + 8 + 2); + put(mip, 8, 0x1007 | 0x20000 | 0x80000); put(mip, 12, 8); put(mip, 16, 8); + put(mip, 20, 32); put(mip, 28, 3); put(mip, 80, 4); put(mip, 84, 0x31545844u); + for (int blk = 0; blk < 4; ++blk) put(mip, 128 + blk * 8, 0x000007e0); // level 0 green + put(mip, 160, 0x0000001f); // level 1 blue + mip[168] = 0x00; mip[169] = 0xf8; // level 2 red + auto im = decode(mip); + assert(im.w == 8 && im.h == 8 && im.file_mip_levels == 3 && im.mips.size() == 2); + pixel(im, 63, 0, 255, 0, 255); + assert(im.mips[0].size() == 4 * 4 * 4 && im.mips[0][2] == 255 && im.mips[0][0] == 0); + assert(im.mips[1].size() == 2 * 2 * 4 && im.mips[1][0] == 255 && im.mips[1][1] == 0); + put(mip, 8, 0x1007 | 0x80000); // no DDSD_MIPMAPCOUNT -> only level 0 is filled + im = decode(mip); + assert(im.file_mip_levels == 1 && im.mips.empty()); + assert(decode(a1555).file_mip_levels == 0); // non-DXT -> D3DX full-chain path + } + std::puts("PASS: dxt_decode_test (masked RGB, pitch, invalid headers, DXT1/3/5, mip levels)"); } diff --git a/native_render/main.cpp b/native_render/main.cpp index a6c91304..a1eda698 100644 --- a/native_render/main.cpp +++ b/native_render/main.cpp @@ -2578,17 +2578,13 @@ private: } #endif if (!decoded.ok()) return it->second; - bool is_ui_tex = false; - if (texture_name.rfind("icon/", 0) == 0 || - texture_name.rfind("d:/ymir work/ui/", 0) == 0 || - texture_name.rfind("d:\\ymir work\\ui\\", 0) == 0 || - texture_name.rfind("locale/", 0) == 0 || - texture_name.rfind("locale\\", 0) == 0 || - texture_name.rfind("mem:", 0) == 0 || - (decoded.w <= 128 && decoded.h <= 128)) { - is_ui_tex = true; - } - upload_texture(decoded.w, decoded.h, decoded.rgba.data(), it->second, true, !is_ui_tex); + // 40250 CGraphicImageTexture: DXT DDS -> CreateDDSTexture with the file's levels; + // other files -> D3DXCreateTextureFromFileInMemoryEx(D3DX_DEFAULT) = full chain; + // memory images (mem:, CreateTexture(w, h, 1, ...)) -> one level. + const bool is_memory_tex = texture_name.rfind("mem:", 0) == 0; + upload_texture( + decoded.w, decoded.h, decoded.rgba.data(), it->second, true, + !is_memory_tex && decoded.file_mip_levels == 0, &decoded.mips); it->second.descriptor = allocate_texture_descriptor_set(it->second.view, fallback_texture_.view); it->second.ui_descriptor = allocate_ui_texture_descriptor_set(it->second.view, fallback_texture_.view); return it->second; @@ -2681,11 +2677,15 @@ private: const std::uint8_t* rgba, GpuTexture& texture, bool count_upload, - bool generate_mips = true) { - const VkDeviceSize byte_size = VkDeviceSize(width) * height * 4; + bool generate_mips = true, + const std::vector>* file_mips = nullptr) { + const bool has_file_mips = !generate_mips && file_mips && !file_mips->empty(); + VkDeviceSize byte_size = VkDeviceSize(width) * height * 4; + if (has_file_mips) + for (const auto& level : *file_mips) byte_size += level.size(); const std::uint32_t mip_levels = generate_mips ? (static_cast(std::floor(std::log2(std::max(width, height)))) + 1u) - : 1u; + : has_file_mips ? 1u + static_cast(file_mips->size()) : 1u; VkBuffer staging_buffer = VK_NULL_HANDLE; VkDeviceMemory staging_memory = VK_NULL_HANDLE; VkBufferCreateInfo buffer_info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; @@ -2703,7 +2703,11 @@ private: check(vkBindBufferMemory(device_, staging_buffer, staging_memory, 0), "vkBindBufferMemory staging"); void* mapped = nullptr; check(vkMapMemory(device_, staging_memory, 0, byte_size, 0, &mapped), "vkMapMemory staging"); - std::memcpy(mapped, rgba, byte_size); + std::memcpy(mapped, rgba, VkDeviceSize(width) * height * 4); + if (has_file_mips) { + auto* dst = static_cast(mapped) + VkDeviceSize(width) * height * 4; + for (const auto& level : *file_mips) { std::memcpy(dst, level.data(), level.size()); dst += level.size(); } + } vkUnmapMemory(device_, staging_memory); VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; @@ -2746,14 +2750,22 @@ private: upload_cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_transfer); - VkBufferImageCopy region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageExtent = {width, height, 1}; - vkCmdCopyBufferToImage(upload_cmd, staging_buffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + std::vector regions(has_file_mips ? mip_levels : 1u); + VkDeviceSize region_offset = 0; + for (std::uint32_t i = 0; i < regions.size(); ++i) { + const std::uint32_t lw = std::max(1u, width >> i), lh = std::max(1u, height >> i); + regions[i].bufferOffset = region_offset; + regions[i].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1}; + regions[i].imageExtent = {lw, lh, 1}; + region_offset += VkDeviceSize(lw) * lh * 4; + } + vkCmdCopyBufferToImage( + upload_cmd, staging_buffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(regions.size()), regions.data()); std::int32_t mip_w = static_cast(width); std::int32_t mip_h = static_cast(height); - for (std::uint32_t i = 1; i < mip_levels; ++i) { + for (std::uint32_t i = 1; generate_mips && i < mip_levels; ++i) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.image = texture.image; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; @@ -2802,7 +2814,9 @@ private: to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_shader.image = texture.image; - to_shader.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip_levels - 1, 1, 0, 1}; + to_shader.subresourceRange = generate_mips + ? VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, mip_levels - 1, 1, 0, 1} + : VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1}; vkCmdPipelineBarrier( upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_shader);