From f02245db8131c2847c9c10af55473dc47318067b Mon Sep 17 00:00:00 2001 From: iwubcode Date: Sat, 13 Jan 2024 00:32:14 -0600 Subject: [PATCH] VideoCommon: add an object called 'SceneDumper' that allows someone to dump a Dolphin draw call out to a GLTF file --- .../GraphicsModEditor/SceneDumper.cpp | 825 ++++++++++++++++++ .../GraphicsModEditor/SceneDumper.h | 93 ++ 2 files changed, 918 insertions(+) create mode 100644 Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.cpp create mode 100644 Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.h diff --git a/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.cpp b/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.cpp new file mode 100644 index 0000000000..980308253d --- /dev/null +++ b/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.cpp @@ -0,0 +1,825 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/GraphicsModEditor/SceneDumper.h" + +#include +#include +#include + +#include +#include +#include + +#include "Common/CommonPaths.h" +#include "Common/EnumUtils.h" +#include "Common/FileUtil.h" +#include "Common/Logging/Log.h" +#include "Core/ConfigManager.h" + +#include "VideoCommon/AbstractTexture.h" +#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModBackend.h" +#include "VideoCommon/OpcodeDecoding.h" +#include "VideoCommon/TextureUtils.h" +#include "VideoCommon/VertexManagerBase.h" +#include "VideoCommon/VideoEvents.h" + +namespace +{ +int ComponentFormatAndIntegerToComponentType(ComponentFormat format, bool integer) +{ + switch (format) + { + case ComponentFormat::Byte: + return TINYGLTF_COMPONENT_TYPE_BYTE; + case ComponentFormat::InvalidFloat5: + case ComponentFormat::InvalidFloat6: + case ComponentFormat::InvalidFloat7: + case ComponentFormat::Float: + { + if (integer) + { + return TINYGLTF_COMPONENT_TYPE_INT; + } + else + { + return TINYGLTF_COMPONENT_TYPE_FLOAT; + } + } + case ComponentFormat::Short: + return TINYGLTF_COMPONENT_TYPE_SHORT; + case ComponentFormat::UByte: + return TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + case ComponentFormat::UShort: + return TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + }; + + return -1; +} + +int ComponentCountToType(std::size_t count) +{ + if (count == 1) + return TINYGLTF_TYPE_SCALAR; + else if (count == 2) + return TINYGLTF_TYPE_VEC2; + else if (count == 3) + return TINYGLTF_TYPE_VEC3; + else if (count == 4) + return TINYGLTF_TYPE_VEC4; + return -1; +} + +int PrimitiveTypeToMode(PrimitiveType type) +{ + switch (type) + { + case PrimitiveType::Points: + return TINYGLTF_MODE_POINTS; + case PrimitiveType::Lines: + return TINYGLTF_MODE_LINE; + case PrimitiveType::Triangles: + return TINYGLTF_MODE_TRIANGLES; + case PrimitiveType::TriangleStrip: + return TINYGLTF_MODE_TRIANGLE_STRIP; + }; + + return -1; +} +} // namespace + +namespace GraphicsModEditor +{ +struct SceneDumper::SceneDumperImpl +{ + SceneDumperImpl() + { + m_model.asset.version = "2.0"; + m_model.asset.generator = "Dolphin emulator"; + } + + using NodeList = std::vector; + std::map> m_xfb_hash_to_scene; + NodeList m_current_node_list; + tinygltf::Model m_model; + + bool AreAllXFBsCapture(std::span xfbs_presented) + { + if (m_xfb_hash_to_scene.empty()) + return false; + + return std::all_of(xfbs_presented.begin(), xfbs_presented.end(), + [this](const std::string& xfb_presented) { + return m_xfb_hash_to_scene.contains(xfb_presented); + }); + } + + void SaveScenesToFile(const std::string& path, std::span xfbs_presented) + { + const bool embed_images = false; + const bool embed_buffers = false; + const bool pretty_print = true; + const bool write_binary = false; + + for (const auto& xfb_presented : xfbs_presented) + { + if (const auto it = m_xfb_hash_to_scene.find(xfb_presented); it != m_xfb_hash_to_scene.end()) + { + for (auto&& node : it->second) + { + m_model.nodes.push_back(std::move(node)); + } + } + } + + tinygltf::TinyGLTF gltf; + if (!gltf.WriteGltfSceneToFile(&m_model, path, embed_images, embed_buffers, pretty_print, + write_binary)) + { + ERROR_LOG_FMT(VIDEO, "Failed to write GLTF file to '{}'", path); + } + } +}; + +SceneDumper::SceneDumper() : m_index_buffer(VertexManagerBase::MAXIBUFFERSIZE) +{ + m_impl = std::make_unique(); + m_index_generator.Init(true); + m_index_generator.Start(m_index_buffer.data()); +} + +SceneDumper::~SceneDumper() = default; + +bool SceneDumper::IsDrawCallInRecording(GraphicsModSystem::DrawCallID draw_call_id) const +{ + return m_record_request.m_draw_call_ids.empty() || + m_record_request.m_draw_call_ids.contains(draw_call_id); +} + +void SceneDumper::AddDataToRecording(GraphicsModSystem::DrawCallID draw_call_id, + const GraphicsModSystem::DrawDataView& draw_data, + AdditionalDrawData additional_draw_data) +{ + const auto& declaration = draw_data.vertex_format->GetVertexDeclaration(); + + // Skip 2 point position elements for now + if (declaration.position.enable && declaration.position.components == 2) + return; + + if (m_record_request.m_dump_gpu_skinning_as_json) + { + m_gpu_skinning_dumper.AddData(draw_call_id, draw_data, m_record_request.m_apply_gpu_skinning, + std::span(m_index_generator.GetIndexDataStart(), + m_index_generator.GetIndexLen())); + } + + // Initial primitive data + tinygltf::Primitive primitive; + if (draw_data.uid->rasterization_state.primitive == PrimitiveType::TriangleStrip) + { + // Triangle strips are used for primitive restart but we don't support that in GLTF + primitive.mode = PrimitiveTypeToMode(PrimitiveType::Triangles); + } + else + { + primitive.mode = PrimitiveTypeToMode(draw_data.uid->rasterization_state.primitive); + } + primitive.indices = static_cast(m_impl->m_model.accessors.size()); + + u32 available_texcoords = 0; + for (std::size_t i = 0; i < declaration.texcoords.size(); i++) + { + if (declaration.texcoords[i].enable && declaration.texcoords[i].components == 2) + { + available_texcoords++; + } + } + + // Material data + if (draw_data.textures.empty() || available_texcoords == 0 || + !m_record_request.m_include_materials) + { + const std::string default_material_name = "Dolphin_Default_Material"; + if (const auto iter = m_materialhash_to_material_id.find(default_material_name); + iter != m_materialhash_to_material_id.end()) + { + primitive.material = iter->second; + } + else + { + tinygltf::Material material; + material.pbrMetallicRoughness.baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f}; + material.doubleSided = true; + material.name = default_material_name; + + primitive.material = static_cast(m_impl->m_model.materials.size()); + m_impl->m_model.materials.push_back(material); + m_materialhash_to_material_id[default_material_name] = primitive.material; + } + } + else + { + std::string texture_material_name = ""; + for (const auto& texture : draw_data.textures) + { + texture_material_name += texture.hash_name; + } + if (const auto iter = m_materialhash_to_material_id.find(texture_material_name); + iter != m_materialhash_to_material_id.end()) + { + primitive.material = iter->second; + } + else + { + tinygltf::Material material; + material.pbrMetallicRoughness.baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f}; + material.doubleSided = true; + material.name = texture_material_name; + if (draw_data.uid->blending_state.blendenable && m_record_request.m_enable_blending) + { + material.alphaMode = "BLEND"; + } + for (const auto& texture : draw_data.textures) + { + if (material.pbrMetallicRoughness.baseColorTexture.index != -1) + { + break; + } + + if (const auto texture_iter = m_texturehash_to_texture_id.find(texture.hash_name); + texture_iter != m_texturehash_to_texture_id.end()) + { + material.pbrMetallicRoughness.baseColorTexture.index = texture_iter->second; + } + else + { + VideoCommon::TextureUtils::DumpTexture(*texture.texture_data, + std::string{texture.hash_name}, 0, false); + + tinygltf::Texture tinygltf_texture; + tinygltf_texture.source = static_cast(m_impl->m_model.images.size()); + + tinygltf::Image image; + image.uri = fmt::format("{}.png", texture.hash_name); + m_impl->m_model.images.push_back(image); + + material.pbrMetallicRoughness.baseColorTexture.index = + static_cast(m_impl->m_model.textures.size()); + m_impl->m_model.textures.push_back(tinygltf_texture); + + m_texturehash_to_texture_id[std::string{texture.hash_name}] = + material.pbrMetallicRoughness.baseColorTexture.index; + } + } + primitive.material = static_cast(m_impl->m_model.materials.size()); + m_materialhash_to_material_id[texture_material_name] = primitive.material; + m_impl->m_model.materials.push_back(material); + } + } + + // Index data + tinygltf::Accessor index_accessor; + index_accessor.name = fmt::format("Accessor 'INDEX' for {}", Common::ToUnderlying(draw_call_id)); + index_accessor.bufferView = static_cast(m_impl->m_model.bufferViews.size()); + index_accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + index_accessor.byteOffset = 0; + index_accessor.type = TINYGLTF_TYPE_SCALAR; + index_accessor.count = m_index_generator.GetIndexLen(); + + auto indice_ptr = reinterpret_cast(m_index_generator.GetIndexDataStart()); + for (std::size_t indice_index = 0; indice_index < m_index_generator.GetIndexLen(); indice_index++) + { + u16 index = 0; + std::memcpy(&index, indice_ptr + index_accessor.byteOffset, sizeof(u16)); + + if (index_accessor.minValues.empty()) + { + index_accessor.minValues.push_back(static_cast(index)); + } + else + { + u16 last_min = static_cast(index_accessor.minValues[0]); + + if (index < last_min) + index_accessor.minValues[0] = index; + } + + if (index_accessor.maxValues.empty()) + { + index_accessor.maxValues.push_back(static_cast(index)); + } + else + { + u16 last_max = static_cast(index_accessor.maxValues[0]); + + if (index > last_max) + index_accessor.maxValues[0] = index; + } + indice_ptr += sizeof(u16); + } + + m_impl->m_model.accessors.push_back(std::move(index_accessor)); + + tinygltf::BufferView index_buffer_view; + index_buffer_view.buffer = static_cast(m_impl->m_model.buffers.size()); + index_buffer_view.byteLength = m_index_generator.GetIndexLen() * sizeof(u16); + index_buffer_view.byteOffset = 0; + index_buffer_view.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + m_impl->m_model.bufferViews.push_back(std::move(index_buffer_view)); + + tinygltf::Buffer index_buffer; + + const auto index_data = + reinterpret_cast(m_index_generator.GetIndexDataStart()); + index_buffer.data = {index_data, index_data + index_buffer_view.byteLength}; + m_impl->m_model.buffers.push_back(std::move(index_buffer)); + + // Fill out all vertex data + const std::size_t stride = declaration.stride; + + Common::Vec3 origin_skinned; + if (declaration.position.enable) + { + primitive.attributes["POSITION"] = static_cast(m_impl->m_model.accessors.size()); + + tinygltf::Accessor accessor; + accessor.name = fmt::format("Accessor 'POSITION' for {}", Common::ToUnderlying(draw_call_id)); + accessor.bufferView = static_cast(m_impl->m_model.bufferViews.size()); + accessor.componentType = ComponentFormatAndIntegerToComponentType(declaration.position.type, + declaration.position.integer); + accessor.byteOffset = declaration.position.offset; + accessor.type = ComponentCountToType(declaration.position.components); + accessor.count = draw_data.vertex_data.size(); + + auto vert_ptr = reinterpret_cast(draw_data.vertex_data.data()); + + // If gpu skinning is turned on but transforms is turned off, we need to calculate + // what the positions are when centered at the origin + std::optional min_position_skinned; + std::optional max_position_skinned; + if (m_record_request.m_apply_gpu_skinning && !m_record_request.m_include_transform && + !draw_data.gpu_skinning_position_transform.empty() && declaration.posmtx.enable) + { + for (std::size_t vert_index = 0; vert_index < draw_data.vertex_data.size(); vert_index++) + { + Common::Vec3 vertex_position; + std::memcpy(&vertex_position.x, vert_ptr + accessor.byteOffset, sizeof(float)); + std::memcpy(&vertex_position.y, vert_ptr + sizeof(float) + accessor.byteOffset, + sizeof(float)); + std::memcpy(&vertex_position.z, vert_ptr + sizeof(float) * 2 + accessor.byteOffset, + sizeof(float)); + + u32 gpu_skin_index; + std::memcpy(&gpu_skin_index, vert_ptr + declaration.posmtx.offset, sizeof(u32)); + + Common::Matrix44 position_transform; + for (std::size_t i = 0; i < 3; i++) + { + position_transform.data[i * 4 + 0] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][0]; + position_transform.data[i * 4 + 1] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][1]; + position_transform.data[i * 4 + 2] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][2]; + position_transform.data[i * 4 + 3] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][3]; + } + position_transform.data[12] = 0; + position_transform.data[13] = 0; + position_transform.data[14] = 0; + position_transform.data[15] = 1; + + // Apply the transform to the position + vertex_position = position_transform.Transform(vertex_position, 1); + + if (!min_position_skinned) + { + min_position_skinned = vertex_position; + } + else + { + if (vertex_position.x < min_position_skinned->x) + min_position_skinned->x = vertex_position.x; + if (vertex_position.y < min_position_skinned->y) + min_position_skinned->y = vertex_position.y; + if (vertex_position.z < min_position_skinned->z) + min_position_skinned->z = vertex_position.z; + } + + if (!max_position_skinned) + { + max_position_skinned = vertex_position; + } + else + { + if (vertex_position.x > max_position_skinned->x) + max_position_skinned->x = vertex_position.x; + if (vertex_position.y > max_position_skinned->y) + max_position_skinned->y = vertex_position.y; + if (vertex_position.z > max_position_skinned->z) + max_position_skinned->z = vertex_position.z; + } + vert_ptr += stride; + } + } + if (min_position_skinned && max_position_skinned) + { + origin_skinned = (*max_position_skinned - *min_position_skinned) / 2.0f; + } + else if (min_position_skinned) + { + origin_skinned = -*min_position_skinned / 2.0f; + } + else if (max_position_skinned) + { + origin_skinned = *max_position_skinned / 2.0f; + } + + vert_ptr = reinterpret_cast(draw_data.vertex_data.data()); + for (std::size_t vert_index = 0; vert_index < draw_data.vertex_data.size(); vert_index++) + { + Common::Vec3 vertex_position; + std::memcpy(&vertex_position.x, vert_ptr + accessor.byteOffset, sizeof(float)); + std::memcpy(&vertex_position.y, vert_ptr + sizeof(float) + accessor.byteOffset, + sizeof(float)); + std::memcpy(&vertex_position.z, vert_ptr + sizeof(float) * 2 + accessor.byteOffset, + sizeof(float)); + + if (m_record_request.m_apply_gpu_skinning && + !draw_data.gpu_skinning_position_transform.empty() && declaration.posmtx.enable) + { + u32 gpu_skin_index; + std::memcpy(&gpu_skin_index, vert_ptr + declaration.posmtx.offset, sizeof(u32)); + + Common::Matrix44 position_transform; + for (std::size_t i = 0; i < 3; i++) + { + position_transform.data[i * 4 + 0] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][0]; + position_transform.data[i * 4 + 1] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][1]; + position_transform.data[i * 4 + 2] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][2]; + position_transform.data[i * 4 + 3] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][3]; + } + position_transform.data[12] = 0; + position_transform.data[13] = 0; + position_transform.data[14] = 0; + position_transform.data[15] = 1; + + // Apply the transform to the position + vertex_position = position_transform.Transform(vertex_position, 1); + + if (!m_record_request.m_include_transform) + { + vertex_position -= origin_skinned; + } + } + + if (accessor.minValues.empty()) + { + accessor.minValues.push_back(static_cast(vertex_position.x)); + accessor.minValues.push_back(static_cast(vertex_position.y)); + accessor.minValues.push_back(static_cast(vertex_position.z)); + } + else + { + const float last_min_x = static_cast(accessor.minValues[0]); + const float last_min_y = static_cast(accessor.minValues[1]); + const float last_min_z = static_cast(accessor.minValues[2]); + + if (vertex_position.x < last_min_x) + accessor.minValues[0] = vertex_position.x; + if (vertex_position.y < last_min_y) + accessor.minValues[1] = vertex_position.y; + if (vertex_position.z < last_min_z) + accessor.minValues[2] = vertex_position.z; + } + + if (accessor.maxValues.empty()) + { + accessor.maxValues.push_back(static_cast(vertex_position.x)); + accessor.maxValues.push_back(static_cast(vertex_position.y)); + accessor.maxValues.push_back(static_cast(vertex_position.z)); + } + else + { + const float last_max_x = static_cast(accessor.maxValues[0]); + const float last_max_y = static_cast(accessor.maxValues[1]); + const float last_max_z = static_cast(accessor.maxValues[2]); + + if (vertex_position.x > last_max_x) + accessor.maxValues[0] = vertex_position.x; + if (vertex_position.y > last_max_y) + accessor.maxValues[1] = vertex_position.y; + if (vertex_position.z > last_max_z) + accessor.maxValues[2] = vertex_position.z; + } + vert_ptr += stride; + } + + m_impl->m_model.accessors.push_back(std::move(accessor)); + } + + static std::array color_names{"COLOR_0", "COLOR_1"}; + for (std::size_t i = 0; i < declaration.colors.size(); i++) + { + if (declaration.colors[i].enable) + { + primitive.attributes[color_names[i]] = static_cast(m_impl->m_model.accessors.size()); + + tinygltf::Accessor accessor; + accessor.name = + fmt::format("Accessor '{}' for {}", color_names[i], Common::ToUnderlying(draw_call_id)); + accessor.bufferView = static_cast(m_impl->m_model.bufferViews.size()); + accessor.componentType = ComponentFormatAndIntegerToComponentType( + declaration.colors[i].type, declaration.colors[i].integer); + accessor.byteOffset = declaration.colors[i].offset; + accessor.type = ComponentCountToType(declaration.colors[i].components); + accessor.count = draw_data.vertex_data.size(); + accessor.normalized = declaration.colors[i].type == ComponentFormat::UByte || + declaration.colors[i].type == ComponentFormat::UShort || + declaration.colors[i].type == ComponentFormat::Byte || + declaration.colors[i].type == ComponentFormat::Short; + + m_impl->m_model.accessors.push_back(std::move(accessor)); + } + } + + // TODO: tangent/binormal? + static std::array norm_names{"NORMAL"}; + for (std::size_t i = 0; i < declaration.normals.size(); i++) + { + if (declaration.normals[i].enable) + { + primitive.attributes[norm_names[i]] = static_cast(m_impl->m_model.accessors.size()); + + tinygltf::Accessor accessor; + accessor.name = + fmt::format("Accessor '{}' for {}", norm_names[i], Common::ToUnderlying(draw_call_id)); + accessor.bufferView = static_cast(m_impl->m_model.bufferViews.size()); + accessor.componentType = ComponentFormatAndIntegerToComponentType( + declaration.normals[i].type, declaration.normals[i].integer); + accessor.byteOffset = declaration.normals[i].offset; + accessor.type = ComponentCountToType(declaration.normals[i].components); + accessor.count = draw_data.vertex_data.size(); + + m_impl->m_model.accessors.push_back(std::move(accessor)); + } + } + + static std::array texcoord_names = { + "TEXCOORD_0", "TEXCOORD_1", "TEXCOORD_2", "TEXCOORD_3", + "TEXCOORD_4", "TEXCOORD_5", "TEXCOORD_6", "TEXCOORD_7", + }; + for (std::size_t i = 0; i < declaration.texcoords.size(); i++) + { + // Ignore 3 component texcoords for now..they are tex matrixes? + if (declaration.texcoords[i].enable && declaration.texcoords[i].components == 2) + { + primitive.attributes[texcoord_names[i]] = static_cast(m_impl->m_model.accessors.size()); + + tinygltf::Accessor accessor; + accessor.name = fmt::format("Accessor '{}' for {}", texcoord_names[i], + Common::ToUnderlying(draw_call_id)); + accessor.bufferView = static_cast(m_impl->m_model.bufferViews.size()); + accessor.componentType = ComponentFormatAndIntegerToComponentType( + declaration.texcoords[i].type, declaration.texcoords[i].integer); + accessor.byteOffset = declaration.texcoords[i].offset; + accessor.type = ComponentCountToType(declaration.texcoords[i].components); + accessor.count = draw_data.vertex_data.size(); + accessor.normalized = declaration.texcoords[i].type == ComponentFormat::UByte || + declaration.texcoords[i].type == ComponentFormat::UShort || + declaration.texcoords[i].type == ComponentFormat::Byte || + declaration.texcoords[i].type == ComponentFormat::Short; + + m_impl->m_model.accessors.push_back(std::move(accessor)); + } + } + + // Vertex buffer data + tinygltf::BufferView vertex_buffer_view; + vertex_buffer_view.buffer = static_cast(m_impl->m_model.buffers.size()); + vertex_buffer_view.byteLength = draw_data.vertex_data.size() * stride; + vertex_buffer_view.byteOffset = 0; + vertex_buffer_view.byteStride = stride; + vertex_buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + m_impl->m_model.bufferViews.push_back(std::move(vertex_buffer_view)); + + tinygltf::Buffer buffer; + const auto vert_data = static_cast(draw_data.vertex_data.data()); + buffer.data = {vert_data, vert_data + vertex_buffer_view.byteLength}; + + // If gpu skinning is turned on but transforms is turned off, we need to calculate + // what the positions are when centered at the origin + if (m_record_request.m_apply_gpu_skinning && !draw_data.gpu_skinning_position_transform.empty() && + declaration.posmtx.enable && declaration.position.enable) + { + auto vert_ptr = buffer.data.data(); + for (std::size_t vert_index = 0; vert_index < draw_data.vertex_data.size(); vert_index++) + { + Common::Vec3 vertex_position; + std::memcpy(&vertex_position.x, vert_ptr + declaration.position.offset, sizeof(float)); + std::memcpy(&vertex_position.y, vert_ptr + sizeof(float) + declaration.position.offset, + sizeof(float)); + std::memcpy(&vertex_position.z, vert_ptr + sizeof(float) * 2 + declaration.position.offset, + sizeof(float)); + + u32 gpu_skin_index; + std::memcpy(&gpu_skin_index, vert_ptr + declaration.posmtx.offset, sizeof(u32)); + + Common::Matrix44 position_transform; + for (std::size_t i = 0; i < 3; i++) + { + position_transform.data[i * 4 + 0] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][0]; + position_transform.data[i * 4 + 1] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][1]; + position_transform.data[i * 4 + 2] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][2]; + position_transform.data[i * 4 + 3] = + draw_data.gpu_skinning_position_transform[gpu_skin_index + i][3]; + } + position_transform.data[12] = 0; + position_transform.data[13] = 0; + position_transform.data[14] = 0; + position_transform.data[15] = 1; + + // Apply the transform to the position + vertex_position = position_transform.Transform(vertex_position, 1); + + if (!m_record_request.m_include_transform) + { + vertex_position -= origin_skinned; + } + + std::memcpy(vert_ptr + declaration.position.offset, &vertex_position.x, sizeof(float)); + std::memcpy(vert_ptr + sizeof(float) + declaration.position.offset, &vertex_position.y, + sizeof(float)); + std::memcpy(vert_ptr + sizeof(float) * 2 + declaration.position.offset, &vertex_position.z, + sizeof(float)); + + if (declaration.normals[0].enable) + { + Common::Vec3 vertex_normal; + std::memcpy(&vertex_normal.x, vert_ptr + declaration.normals[0].offset, sizeof(float)); + std::memcpy(&vertex_normal.y, vert_ptr + sizeof(float) + declaration.normals[0].offset, + sizeof(float)); + std::memcpy(&vertex_normal.z, vert_ptr + sizeof(float) * 2 + declaration.normals[0].offset, + sizeof(float)); + + Common::Matrix33 normal_transform; + for (std::size_t i = 0; i < 3; i++) + { + normal_transform.data[i * 3 + 0] = + draw_data.gpu_skinning_normal_transform[gpu_skin_index + i][0]; + normal_transform.data[i * 3 + 1] = + draw_data.gpu_skinning_normal_transform[gpu_skin_index + i][1]; + normal_transform.data[i * 3 + 2] = + draw_data.gpu_skinning_normal_transform[gpu_skin_index + i][2]; + } + + // Apply the transform to the normal + vertex_normal = normal_transform * vertex_normal; + + // Save into output + std::memcpy(vert_ptr + declaration.normals[0].offset, &vertex_normal.x, sizeof(float)); + std::memcpy(vert_ptr + sizeof(float) + declaration.normals[0].offset, &vertex_normal.y, + sizeof(float)); + std::memcpy(vert_ptr + sizeof(float) * 2 + declaration.normals[0].offset, &vertex_normal.z, + sizeof(float)); + } + + vert_ptr += stride; + } + } + + m_impl->m_model.buffers.push_back(std::move(buffer)); + + // Node data + tinygltf::Node node; + node.name = fmt::format("Node {} for xfb {}", Common::ToUnderlying(draw_call_id), + m_xfbs_since_recording_present); + node.mesh = static_cast(m_impl->m_model.meshes.size()); + + // We expect to get data as a 3x3 if there's a global transform + if (m_record_request.m_include_transform && additional_draw_data.transform.size() == 12) + { + // GLTF expects to be passed a 4x4 + node.matrix.reserve(16); + + for (int w = 0; w < 4; w++) + { + for (int h = 0; h < 3; h++) + { + node.matrix.push_back(additional_draw_data.transform[w + h * 4]); + } + if (w == 3) + { + node.matrix.push_back(1); + } + else + { + node.matrix.push_back(0); + } + } + } + m_impl->m_current_node_list.push_back(std::move(node)); + + // Mesh data + tinygltf::Mesh mesh; + mesh.name = fmt::format("Mesh {}", Common::ToUnderlying(draw_call_id)); + mesh.primitives.push_back(std::move(primitive)); + m_impl->m_model.meshes.push_back(std::move(mesh)); +} + +void SceneDumper::Record(const SceneRecordingRequest& request) +{ + const std::string path_prefix = + File::GetUserPath(D_DUMPMESHES_IDX) + SConfig::GetInstance().GetGameID(); + + const std::time_t start_time = std::time(nullptr); + const std::string base_name = + fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}", path_prefix, fmt::localtime(start_time)); + + const std::string gltf_path = fmt::format("{}.gltf", base_name); + m_scene_save_path = gltf_path; + + m_gpu_skinning_dumper.SetOutputPath(fmt::format("{}.json", base_name)); + + m_record_request = request; + m_recording_state = RecordingState::WANTS_RECORDING; +} + +bool SceneDumper::IsRecording() const +{ + return m_recording_state == RecordingState::IS_RECORDING; +} + +void SceneDumper::OnXFBCreated(const std::string& hash) +{ + if (m_recording_state != RecordingState::IS_RECORDING) + { + return; + } + + m_xfbs_since_recording_present++; + + // We saw a XFB create without any data and we just started capturing + // ignore it + if (m_impl->m_current_node_list.empty() && m_xfbs_since_recording_present == 1) + { + return; + } + + // When our frame present happens, we might be in the middle of an xfb + // which means this initial create won't have all the data that we expect + // it to have. Skip the first xfb and start collecting data after that + // This ensures our capture has all data it needs + if (m_xfbs_since_recording_present > 1) + { + m_impl->m_xfb_hash_to_scene.try_emplace(hash, std::move(m_impl->m_current_node_list)); + } + m_impl->m_current_node_list = {}; +} + +void SceneDumper::OnFramePresented(std::span xfbs_presented) +{ + if (m_recording_state == RecordingState::IS_RECORDING) + { + // If all our xfbs aren't captured in this frame, wait for next frame + if (!m_impl->AreAllXFBsCapture(xfbs_presented)) + return; + + m_impl->SaveScenesToFile(m_scene_save_path, xfbs_presented); + + if (m_record_request.m_dump_gpu_skinning_as_json) + m_gpu_skinning_dumper.WriteToFile(); + + m_recording_state = RecordingState::NOT_RECORDING; + + // Release state + m_record_request = {}; + m_materialhash_to_material_id = {}; + m_texturehash_to_texture_id = {}; + m_impl = std::make_unique(); + m_xfbs_since_recording_present = 0; + } + + if (m_recording_state == RecordingState::WANTS_RECORDING) + { + m_recording_state = RecordingState::IS_RECORDING; + } +} + +void SceneDumper::AddIndices(OpcodeDecoder::Primitive primitive, u32 num_vertices) +{ + m_index_generator.AddIndices(primitive, num_vertices); +} + +void SceneDumper::ResetIndices() +{ + m_index_generator.Start(m_index_buffer.data()); +} +} // namespace GraphicsModEditor diff --git a/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.h b/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.h new file mode 100644 index 0000000000..da7d451204 --- /dev/null +++ b/Source/Core/VideoCommon/GraphicsModEditor/SceneDumper.h @@ -0,0 +1,93 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/SmallVector.h" + +#include "VideoCommon/GraphicsModEditor/GpuSkinningDataDumper.h" +#include "VideoCommon/GraphicsModSystem/Types.h" +#include "VideoCommon/IndexGenerator.h" +#include "VideoCommon/NativeVertexFormat.h" +#include "VideoCommon/OpcodeDecoding.h" +#include "VideoCommon/RenderState.h" + +class AbstractTexture; + +namespace GraphicsModSystem +{ +struct DrawData; +} + +namespace GraphicsModEditor +{ + +struct SceneRecordingRequest +{ + std::unordered_set m_draw_call_ids; + + bool m_enable_blending = false; + bool m_apply_gpu_skinning = true; + bool m_include_transform = true; + bool m_include_materials = true; + bool m_ignore_orthographic = false; + bool m_dump_gpu_skinning_as_json = false; +}; +class SceneDumper +{ +public: + SceneDumper(); + ~SceneDumper(); + + struct AdditionalDrawData + { + std::span transform; + }; + bool IsDrawCallInRecording(GraphicsModSystem::DrawCallID draw_call_id) const; + void AddDataToRecording(GraphicsModSystem::DrawCallID draw_call_id, + const GraphicsModSystem::DrawDataView& draw_data, + AdditionalDrawData additional_draw_data); + + void Record(const SceneRecordingRequest& request); + bool IsRecording() const; + + void OnXFBCreated(const std::string& hash); + void OnFramePresented(std::span xfbs_presented); + + void AddIndices(OpcodeDecoder::Primitive primitive, u32 num_vertices); + void ResetIndices(); + +private: + enum RecordingState + { + NOT_RECORDING, + WANTS_RECORDING, + IS_RECORDING + }; + RecordingState m_recording_state = RecordingState::NOT_RECORDING; + + std::map> m_materialhash_to_material_id; + std::map> m_texturehash_to_texture_id; + SceneRecordingRequest m_record_request; + std::string m_scene_save_path; + + GpuSkinningDataDumper m_gpu_skinning_dumper; + + u8 m_xfbs_since_recording_present = 0; + + struct SceneDumperImpl; + std::unique_ptr m_impl; + + std::vector m_index_buffer; + IndexGenerator m_index_generator; +}; +} // namespace GraphicsModEditor