diff --git a/src/common/rendering/v_video.h b/src/common/rendering/v_video.h index 9bdb2ab4b..1a34170a0 100644 --- a/src/common/rendering/v_video.h +++ b/src/common/rendering/v_video.h @@ -227,6 +227,7 @@ public: virtual void PostProcessScene(bool swscene, int fixedcm, float flash, const std::function &afterBloomDrawEndScene2D) { if (afterBloomDrawEndScene2D) afterBloomDrawEndScene2D(); } virtual int GetLevelMeshPipelineID(const MeshApplyData& applyData, const SurfaceUniforms& surfaceUniforms, const FMaterialState& material) { return 0; } + virtual void DownloadLightmap(int arrayIndex, uint16_t* buffer) { } void ScaleCoordsFromWindow(int16_t &x, int16_t &y); diff --git a/src/common/rendering/vulkan/textures/vk_texture.cpp b/src/common/rendering/vulkan/textures/vk_texture.cpp index b10cfc948..eee217749 100644 --- a/src/common/rendering/vulkan/textures/vk_texture.cpp +++ b/src/common/rendering/vulkan/textures/vk_texture.cpp @@ -267,3 +267,40 @@ void VkTextureManager::CreateLightmap(int newLMTextureSize, int newLMTextureCoun .AddImage(&Lightmap, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false, 0, 1, 0, LMTextureCount) .Execute(cmdbuffer); } + +void VkTextureManager::DownloadLightmap(int arrayIndex, uint16_t* buffer) +{ + unsigned int totalSize = LMTextureSize * LMTextureSize * 4; + + auto stagingBuffer = BufferBuilder() + .Size(totalSize * sizeof(uint16_t)) + .Usage(VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_CPU_ONLY) + .DebugName("DownloadLightmap") + .Create(fb->GetDevice()); + + auto cmdbuffer = fb->GetCommands()->GetTransferCommands(); + + PipelineBarrier() + .AddImage(Lightmap.Image.get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, arrayIndex, 1) + .Execute(cmdbuffer, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + VkBufferImageCopy region = {}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.baseArrayLayer = arrayIndex; + region.imageSubresource.layerCount = 1; + region.imageSubresource.mipLevel = 0; + region.imageExtent.width = LMTextureSize; + region.imageExtent.height = LMTextureSize; + region.imageExtent.depth = 1; + cmdbuffer->copyImageToBuffer(Lightmap.Image->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer->buffer, 1, ®ion); + + PipelineBarrier() + .AddImage(Lightmap.Image.get(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, arrayIndex, 1) + .Execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + fb->GetCommands()->WaitForCommands(false); + + uint16_t* srcdata = (uint16_t*)stagingBuffer->Map(0, totalSize * sizeof(uint16_t)); + memcpy(buffer, srcdata, totalSize * sizeof(uint16_t)); + stagingBuffer->Unmap(); +} diff --git a/src/common/rendering/vulkan/textures/vk_texture.h b/src/common/rendering/vulkan/textures/vk_texture.h index f829503d9..f79163e31 100644 --- a/src/common/rendering/vulkan/textures/vk_texture.h +++ b/src/common/rendering/vulkan/textures/vk_texture.h @@ -24,6 +24,7 @@ public: void BeginFrame(); void CreateLightmap(int newLMTextureSize, int newLMTextureCount, TArray&& newPixelData); + void DownloadLightmap(int arrayIndex, uint16_t* buffer); VkTextureImage* GetTexture(const PPTextureType& type, PPTexture* tex); VkFormat GetTextureFormat(PPTexture* texture); diff --git a/src/common/rendering/vulkan/vk_renderdevice.cpp b/src/common/rendering/vulkan/vk_renderdevice.cpp index 770bd4858..c4f9f485a 100644 --- a/src/common/rendering/vulkan/vk_renderdevice.cpp +++ b/src/common/rendering/vulkan/vk_renderdevice.cpp @@ -649,6 +649,11 @@ void VulkanRenderDevice::SetSceneRenderTarget(bool useSSAO) mRenderState->SetRenderTarget(&GetBuffers()->SceneColor, GetBuffers()->SceneDepthStencil.View.get(), GetBuffers()->GetWidth(), GetBuffers()->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, GetBuffers()->GetSceneSamples()); } +void VulkanRenderDevice::DownloadLightmap(int arrayIndex, uint16_t* buffer) +{ + mTextureManager->DownloadLightmap(arrayIndex, buffer); +} + int VulkanRenderDevice::GetBindlessTextureIndex(FMaterial* material, int clampmode, int translation) { FMaterialState materialState; diff --git a/src/common/rendering/vulkan/vk_renderdevice.h b/src/common/rendering/vulkan/vk_renderdevice.h index a4d9559bb..df2868c07 100644 --- a/src/common/rendering/vulkan/vk_renderdevice.h +++ b/src/common/rendering/vulkan/vk_renderdevice.h @@ -94,6 +94,7 @@ public: int GetBindlessTextureIndex(FMaterial* material, int clampmode, int translation) override; int GetLevelMeshPipelineID(const MeshApplyData& applyData, const SurfaceUniforms& surfaceUniforms, const FMaterialState& material) override; + void DownloadLightmap(int arrayIndex, uint16_t* buffer) override; const VkPipelineKey& GetLevelMeshPipelineKey(int id) const; diff --git a/src/rendering/hwrenderer/doom_levelmesh.cpp b/src/rendering/hwrenderer/doom_levelmesh.cpp index f33f56ff5..fbc70832b 100644 --- a/src/rendering/hwrenderer/doom_levelmesh.cpp +++ b/src/rendering/hwrenderer/doom_levelmesh.cpp @@ -94,6 +94,20 @@ CCMD(invalidatelightmap) Printf("Marked %d out of %d tiles for update.\n", count, level.levelMesh->LightmapTiles.Size()); } +CCMD(savelightmap) +{ + if (!RequireLightmap()) return; + + level.levelMesh->SaveLightmapLump(level); +} + +CCMD(deletelightmap) +{ + if (!RequireLightmap()) return; + + level.levelMesh->DeleteLightmapLump(level); +} + void DoomLevelMesh::PrintSurfaceInfo(const LevelMeshSurface* surface) { if (!RequireLevelMesh()) return; @@ -1769,3 +1783,405 @@ void DoomLevelMesh::CreatePortals(FLevelLocals& doomMap) } } } + +class LumpWriter +{ +public: + LumpWriter(size_t size) { buffer.Reserve(size); } + + void Write(const void* data, size_t size) + { + if (pos + size > buffer.size() || size > 0xffffffff) + I_FatalError("LumpWriter ran out of space!"); + memcpy(buffer.data() + pos, data, size); + pos += size; + } + + void Write8(const uint8_t val) { Write(&val, sizeof(uint8_t)); } + void Write16(const short val) { Write(&val, sizeof(uint16_t)); } + void Write32(const int val) { Write(&val, sizeof(uint32_t)); } + void WriteFloat(const float val) { Write(&val, sizeof(float)); } + + TArray DeflateCompress() + { + TArray output; + + enum { BUFFER_SIZE = 8192 }; + uint8_t Buffer[BUFFER_SIZE]; + + mz_stream Stream = {}; + int err = deflateInit(&Stream, 9); + if (err != Z_OK) + I_FatalError("Could not initialize deflate buffer."); + + Stream.next_out = Buffer; + Stream.avail_out = BUFFER_SIZE; + Stream.next_in = buffer.data(); + Stream.avail_in = buffer.size(); + err = mz_deflate(&Stream, 0); + while (Stream.avail_out == 0 && err == Z_OK) + { + AddBytes(output, Buffer, BUFFER_SIZE); + + Stream.next_out = Buffer; + Stream.avail_out = BUFFER_SIZE; + if (Stream.avail_in != 0) + { + err = mz_deflate(&Stream, 0); + } + } + if (err != Z_OK) + I_FatalError("Error deflating data."); + + while (true) + { + err = mz_deflate(&Stream, Z_FINISH); + if (err != Z_OK) + { + break; + } + if (Stream.avail_out == 0) + { + AddBytes(output, Buffer, BUFFER_SIZE); + Stream.next_out = Buffer; + Stream.avail_out = BUFFER_SIZE; + } + } + mz_deflateEnd(&Stream); + AddBytes(output, Buffer, BUFFER_SIZE - Stream.avail_out); + + return output; + } + +private: + static void AddBytes(TArray& output, const void* data, size_t size) + { + int index = output.Reserve(size); + memcpy(&output[index], data, size); + } + + size_t pos = 0; + TArray buffer; +}; + +class MapLump +{ +public: + FString Name; + TArray Data; +}; + +TArray LoadMapLumps(FileReader* reader, const char* wadType) +{ + char magic[4] = {}; + uint32_t numlumps = 0; + uint32_t infotableofs = 0; + if (reader->Read(magic, 4) != 4) return {}; + if (memcmp(magic, wadType, 4) != 0) return {}; + if (reader->Read(&numlumps, 4) != 4) return {}; + if (reader->Read(&infotableofs, 4) != 4) return {}; + if (reader->Seek(infotableofs, FileReader::SeekSet) == -1) return {}; + + TArray lumps; + TArray offsets; + lumps.Reserve(numlumps); + offsets.Reserve(numlumps); + + for (uint32_t i = 0; i < numlumps; i++) + { + uint32_t filepos = 0, lumpsize = 0; + char name[9] = {}; + if (reader->Read(&filepos, 4) != 4) return {}; + if (reader->Read(&lumpsize, 4) != 4) return {}; + if (reader->Read(name, 8) != 8) return {}; + + offsets[i] = filepos; + lumps[i].Name = name; + lumps[i].Data.Reserve(lumpsize); + } + + for (uint32_t i = 0; i < numlumps; i++) + { + if (reader->Seek(offsets[i], FileReader::SeekSet) == -1) return {}; + if (reader->Read(lumps[i].Data.data(), lumps[i].Data.size()) != lumps[i].Data.size()) return {}; + } + + return lumps; +} + +void SaveMapLumps(FileWriter* writer, const TArray& lumps, const char* wadType) +{ + uint32_t numlumps = (uint32_t)lumps.size(); + uint32_t infotableofs = 12; + for (uint32_t i = 0; i < numlumps; i++) + infotableofs += lumps[i].Data.size(); + + writer->Write(wadType, 4); + writer->Write(&numlumps, 4); + writer->Write(&infotableofs, 4); + + TArray offsets; + offsets.Reserve(numlumps); + + uint32_t pos = 12; + for (uint32_t i = 0; i < numlumps; i++) + { + offsets[i] = pos; + writer->Write(lumps[i].Data.data(), lumps[i].Data.size()); + pos += lumps[i].Data.size(); + } + + for (uint32_t i = 0; i < numlumps; i++) + { + uint32_t filepos = offsets[i]; + uint32_t lumpsize = lumps[i].Data.size(); + char name[8] = {}; + memcpy(name, lumps[i].Name.GetChars(), lumps[i].Name.Len()); + writer->Write(&filepos, 4); + writer->Write(&lumpsize, 4); + writer->Write(name, 8); + } +} + +void DoomLevelMesh::SaveLightmapLump(FLevelLocals& doomMap) +{ + /* + // LIGHTMAP V3 pseudo-C specification: + + struct LightmapLump + { + int version = 2; + uint32_t tileCount; + uint32_t pixelCount; + uint32_t uvCount; + SurfaceEntry surfaces[surfaceCount]; + uint16_t pixels[pixelCount * 3]; + }; + + struct TileEntry + { + uint32_t type, typeIndex; + uint32_t controlSector; // 0xFFFFFFFF is none + uint16_t width, height; // in pixels + uint32_t pixelsOffset; // offset in pixels array + vec3 translateWorldToLocal; + vec3 projLocalToU; + vec3 projLocalToV; + }; + */ + + LMTextureData.Resize(LMTextureSize * LMTextureSize * LMTextureCount * 4); + for (int arrayIndex = 0; arrayIndex < LMTextureCount; arrayIndex++) + { + screen->DownloadLightmap(arrayIndex, LMTextureData.Data() + arrayIndex * LMTextureSize * LMTextureSize * 4); + } + + // Calculate size of lump + uint32_t tileCount = 0; + uint32_t pixelCount = 0; + + for (unsigned int i = 0; i < LightmapTiles.Size(); i++) + { + LightmapTile* tile = &LightmapTiles[i]; + if (tile->AtlasLocation.ArrayIndex != -1) + { + tileCount++; + pixelCount += tile->AtlasLocation.Area(); + } + } + + const int version = 3; + + const uint32_t headerSize = sizeof(int) + 2 * sizeof(uint32_t); + const uint32_t bytesPerTileEntry = sizeof(uint32_t) * 4 + sizeof(uint16_t) * 2 + sizeof(float) * 9; + const uint32_t bytesPerPixel = sizeof(uint16_t) * 3; // F16 RGB + + uint32_t lumpSize = headerSize + tileCount * bytesPerTileEntry + pixelCount * bytesPerPixel; + + LumpWriter lumpFile(lumpSize); + + // Write header + lumpFile.Write32(version); + lumpFile.Write32(tileCount); + lumpFile.Write32(pixelCount); + + // Write tiles + uint32_t pixelsOffset = 0; + + for (unsigned int i = 0; i < LightmapTiles.Size(); i++) + { + LightmapTile* tile = &LightmapTiles[i]; + + if (tile->AtlasLocation.ArrayIndex == -1) + continue; + + lumpFile.Write32(tile->Binding.Type); + lumpFile.Write32(tile->Binding.TypeIndex); + lumpFile.Write32(tile->Binding.ControlSector); + + lumpFile.Write16(uint16_t(tile->AtlasLocation.Width)); + lumpFile.Write16(uint16_t(tile->AtlasLocation.Height)); + + lumpFile.Write32(pixelsOffset * 3); + + lumpFile.WriteFloat(tile->Transform.TranslateWorldToLocal.X); + lumpFile.WriteFloat(tile->Transform.TranslateWorldToLocal.Y); + lumpFile.WriteFloat(tile->Transform.TranslateWorldToLocal.Z); + + lumpFile.WriteFloat(tile->Transform.ProjLocalToU.X); + lumpFile.WriteFloat(tile->Transform.ProjLocalToU.Y); + lumpFile.WriteFloat(tile->Transform.ProjLocalToU.Z); + + lumpFile.WriteFloat(tile->Transform.ProjLocalToV.X); + lumpFile.WriteFloat(tile->Transform.ProjLocalToV.Y); + lumpFile.WriteFloat(tile->Transform.ProjLocalToV.Z); + + pixelsOffset += tile->AtlasLocation.Area(); + } + + // Write surface pixels + for (unsigned int i = 0; i < LightmapTiles.Size(); i++) + { + LightmapTile* tile = &LightmapTiles[i]; + + if (tile->AtlasLocation.ArrayIndex == -1) + continue; + + const uint16_t* pixels = LMTextureData.Data() + tile->AtlasLocation.ArrayIndex * LMTextureSize * LMTextureSize * 4; + int width = tile->AtlasLocation.Width; + int height = tile->AtlasLocation.Height; + for (int y = 0; y < height; y++) + { + const uint16_t* srcline = pixels + (tile->AtlasLocation.X + (tile->AtlasLocation.Y + y) * LMTextureSize) * 4; + for (int x = 0; x < width; x++) + { + lumpFile.Write16(*(srcline++)); + lumpFile.Write16(*(srcline++)); + lumpFile.Write16(*(srcline++)); + srcline++; + } + } + } + + FString fullpath = GetMapFilename(doomMap); + if (fullpath.Len() == 0) + return; + + Printf("Saving LIGHTMAP lump into %s\n", fullpath.GetChars()); + + FileReader reader; + if (!reader.OpenFile(fullpath.GetChars())) + { + I_Error("Could not open map WAD file"); + return; + } + + TArray lumps = LoadMapLumps(&reader, "PWAD"); + if (lumps.Size() == 0) + { + I_Error("Could not read map WAD file"); + return; + } + + reader.Close(); + + int lightmapIndex = -1; + int endmapIndex = lumps.size(); + FString strLightmap = "LIGHTMAP"; + FString strEndmap = "ENDMAP"; + for (int i = 0, count = lumps.size(); i < count; i++) + { + if (lumps[i].Name == strLightmap) + lightmapIndex = i; + else if (lumps[i].Name == strEndmap) + endmapIndex = i; + } + + if (lightmapIndex != -1) + lumps[lightmapIndex].Data = lumpFile.DeflateCompress(); + else + lumps.Insert(endmapIndex, { "LIGHTMAP", lumpFile.DeflateCompress()}); + + std::unique_ptr writer(FileWriter::Open(fullpath.GetChars())); + if (writer) + { + SaveMapLumps(writer.get(), lumps, "PWAD"); + } + else + { + I_Error("Could not write map WAD file"); + } +} + +void DoomLevelMesh::DeleteLightmapLump(FLevelLocals& doomMap) +{ + FString fullpath = GetMapFilename(doomMap); + if (fullpath.Len() == 0) + return; + + Printf("Deleting LIGHTMAP lump into %s\n", fullpath.GetChars()); + + FileReader reader; + if (!reader.OpenFile(fullpath.GetChars())) + { + I_Error("Could not open map WAD file"); + return; + } + + TArray lumps = LoadMapLumps(&reader, "PWAD"); + if (lumps.Size() == 0) + { + I_Error("Could not read map WAD file"); + return; + } + reader.Close(); + + int lightmapIndex = -1; + FString strLightmap = "LIGHTMAP"; + for (int i = 0, count = lumps.size(); i < count; i++) + { + if (lumps[i].Name == strLightmap) + lightmapIndex = i; + } + + if (lightmapIndex == -1) + return; + + lumps.Delete(lightmapIndex); + + std::unique_ptr writer(FileWriter::Open(fullpath.GetChars())); + if (writer) + { + SaveMapLumps(writer.get(), lumps, "PWAD"); + } + else + { + I_Error("Could not write map WAD file"); + } +} + +FString DoomLevelMesh::GetMapFilename(FLevelLocals& doomMap) +{ + const char* mapname = doomMap.MapName.GetChars(); + + FString fmt; + fmt.Format("maps/%s.wad", mapname); + int lump_wad = fileSystem.CheckNumForFullName(fmt.GetChars()); + if (lump_wad == -1) + { + I_Error("Could not find map lump"); + return {}; + } + + int wadnum = fileSystem.GetFileContainer(lump_wad); + if (wadnum == -1) + { + I_Error("Could not find map folder"); + return {}; + } + + FString filename = fileSystem.GetFileFullName(lump_wad); + FString folder = fileSystem.GetResourceFileFullName(wadnum); + FString fullpath = folder + filename; + return fullpath; +} diff --git a/src/rendering/hwrenderer/doom_levelmesh.h b/src/rendering/hwrenderer/doom_levelmesh.h index 8866f5dd2..b1271fa10 100644 --- a/src/rendering/hwrenderer/doom_levelmesh.h +++ b/src/rendering/hwrenderer/doom_levelmesh.h @@ -109,6 +109,10 @@ public: void CreateLights(FLevelLocals& doomMap); + void SaveLightmapLump(FLevelLocals& doomMap); + void DeleteLightmapLump(FLevelLocals& doomMap); + static FString GetMapFilename(FLevelLocals& doomMap); + void FloorHeightChanged(sector_t* sector) override; void CeilingHeightChanged(sector_t* sector) override; void MidTex3DHeightChanged(sector_t* sector) override;