mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +00:00 
			
		
		
		
	Add CiTrace recording support.
This is exposed in the GUI as a new "CiTrace Recording" widget. Playback is implemented by a standalone 3DS homebrew application (which only runs reliably within Citra currently; on an actual 3DS it will often crash still).
This commit is contained in:
		
							parent
							
								
									93d66475d4
								
							
						
					
					
						commit
						902fa4da52
					
				
					 15 changed files with 641 additions and 4 deletions
				
			
		
							
								
								
									
										198
									
								
								src/core/tracer/recorder.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/core/tracer/recorder.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| // Copyright 2015 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <cstring> | ||||
| 
 | ||||
| #include "common/assert.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/logging/log.h" | ||||
| 
 | ||||
| #include "recorder.h" | ||||
| 
 | ||||
| namespace CiTrace { | ||||
| 
 | ||||
| Recorder::Recorder(u32* gpu_registers,     u32 gpu_registers_size, | ||||
|                    u32* lcd_registers,     u32 lcd_registers_size, | ||||
|                    u32* pica_registers,    u32 pica_registers_size, | ||||
|                    u32* vs_program_binary, u32 vs_program_binary_size, | ||||
|                    u32* vs_swizzle_data,   u32 vs_swizzle_data_size, | ||||
|                    u32* vs_float_uniforms, u32 vs_float_uniforms_size, | ||||
|                    u32* gs_program_binary, u32 gs_program_binary_size, | ||||
|                    u32* gs_swizzle_data,   u32 gs_swizzle_data_size, | ||||
|                    u32* gs_float_uniforms, u32 gs_float_uniforms_size) | ||||
|     : gpu_registers(gpu_registers, gpu_registers + gpu_registers_size), | ||||
|       lcd_registers(lcd_registers, lcd_registers + lcd_registers_size), | ||||
|       pica_registers(pica_registers, pica_registers + pica_registers_size), | ||||
|       vs_program_binary(vs_program_binary, vs_program_binary + vs_program_binary_size), | ||||
|       vs_swizzle_data(vs_swizzle_data, vs_swizzle_data + vs_swizzle_data_size), | ||||
|       vs_float_uniforms(vs_float_uniforms, vs_float_uniforms + vs_float_uniforms_size), | ||||
|       gs_program_binary(gs_program_binary, gs_program_binary + gs_program_binary_size), | ||||
|       gs_swizzle_data(gs_swizzle_data, gs_swizzle_data + gs_swizzle_data_size), | ||||
|       gs_float_uniforms(gs_float_uniforms, gs_float_uniforms + gs_float_uniforms_size) { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| void Recorder::Finish(const std::string& filename) { | ||||
|     // Setup CiTrace header
 | ||||
|     CTHeader header; | ||||
|     std::memcpy(header.magic, CTHeader::ExpectedMagicWord(), 4); | ||||
|     header.version = CTHeader::ExpectedVersion(); | ||||
|     header.header_size = sizeof(CTHeader); | ||||
| 
 | ||||
|     // Calculate file offsets
 | ||||
|     auto& initial = header.initial_state_offsets; | ||||
| 
 | ||||
|     initial.gpu_registers_size     = gpu_registers.size(); | ||||
|     initial.lcd_registers_size     = lcd_registers.size(); | ||||
|     initial.pica_registers_size    = pica_registers.size(); | ||||
|     initial.vs_program_binary_size = vs_program_binary.size(); | ||||
|     initial.vs_swizzle_data_size   = vs_swizzle_data.size(); | ||||
|     initial.vs_float_uniforms_size = vs_float_uniforms.size(); | ||||
|     initial.gs_program_binary_size = gs_program_binary.size(); | ||||
|     initial.gs_swizzle_data_size   = gs_swizzle_data.size(); | ||||
|     initial.gs_float_uniforms_size = gs_float_uniforms.size(); | ||||
|     header.stream_size             = stream.size(); | ||||
| 
 | ||||
|     initial.gpu_registers     = sizeof(header); | ||||
|     initial.lcd_registers     = initial.gpu_registers     + initial.gpu_registers_size * sizeof(u32); | ||||
|     initial.pica_registers    = initial.lcd_registers     + initial.lcd_registers_size * sizeof(u32);; | ||||
|     initial.vs_program_binary = initial.pica_registers    + initial.pica_registers_size * sizeof(u32); | ||||
|     initial.vs_swizzle_data   = initial.vs_program_binary + initial.vs_program_binary_size * sizeof(u32); | ||||
|     initial.vs_float_uniforms = initial.vs_swizzle_data   + initial.vs_swizzle_data_size * sizeof(u32); | ||||
|     initial.gs_program_binary = initial.vs_float_uniforms + initial.vs_float_uniforms_size * sizeof(u32); | ||||
|     initial.gs_swizzle_data   = initial.gs_program_binary + initial.gs_program_binary_size * sizeof(u32); | ||||
|     initial.gs_float_uniforms = initial.gs_swizzle_data   + initial.gs_swizzle_data_size * sizeof(u32); | ||||
|     header.stream_offset      = initial.gs_float_uniforms + initial.gs_float_uniforms_size * sizeof(u32); | ||||
| 
 | ||||
|     // Iterate through stream elements, update relevant stream element data
 | ||||
|     for (auto& stream_element : stream) { | ||||
|         switch (stream_element.data.type) { | ||||
|         case MemoryLoad: | ||||
|         { | ||||
|             auto& file_offset = memory_regions[stream_element.hash]; | ||||
|             if (!stream_element.uses_existing_data) { | ||||
|                 file_offset = header.stream_offset; | ||||
|             } | ||||
|             stream_element.data.memory_load.file_offset = file_offset; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         default: | ||||
|             // Other commands don't use any extra data
 | ||||
|             DEBUG_ASSERT(stream_element.extra_data.size() == 0); | ||||
|             break; | ||||
|         } | ||||
|         header.stream_offset += stream_element.extra_data.size(); | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|         // Open file and write header
 | ||||
|         FileUtil::IOFile file(filename, "wb"); | ||||
|         size_t written = file.WriteObject(header); | ||||
|         if (written != 1 || file.Tell() != initial.gpu_registers) | ||||
|             throw "Failed to write header"; | ||||
| 
 | ||||
|         // Write initial state
 | ||||
|         written = file.WriteArray(gpu_registers.data(), gpu_registers.size()); | ||||
|         if (written != gpu_registers.size() || file.Tell() != initial.lcd_registers) | ||||
|             throw "Failed to write GPU registers"; | ||||
| 
 | ||||
|         written = file.WriteArray(lcd_registers.data(), lcd_registers.size()); | ||||
|         if (written != lcd_registers.size() || file.Tell() != initial.pica_registers) | ||||
|             throw "Failed to write LCD registers"; | ||||
| 
 | ||||
|         written = file.WriteArray(pica_registers.data(), pica_registers.size()); | ||||
|         if (written != pica_registers.size() || file.Tell() != initial.vs_program_binary) | ||||
|             throw "Failed to write Pica registers"; | ||||
| 
 | ||||
|         written = file.WriteArray(vs_program_binary.data(), vs_program_binary.size()); | ||||
|         if (written != vs_program_binary.size() || file.Tell() != initial.vs_swizzle_data) | ||||
|             throw "Failed to write vertex shader program binary"; | ||||
| 
 | ||||
|         written = file.WriteArray(vs_swizzle_data.data(), vs_swizzle_data.size()); | ||||
|         if (written != vs_swizzle_data.size() || file.Tell() != initial.vs_float_uniforms) | ||||
|             throw "Failed to write vertex shader swizzle data"; | ||||
| 
 | ||||
|         written = file.WriteArray(vs_float_uniforms.data(), vs_float_uniforms.size()); | ||||
|         if (written != vs_float_uniforms.size() || file.Tell() != initial.gs_program_binary) | ||||
|             throw "Failed to write vertex shader float uniforms"; | ||||
| 
 | ||||
|         written = file.WriteArray(gs_program_binary.data(), gs_program_binary.size()); | ||||
|         if (written != gs_program_binary.size() || file.Tell() != initial.gs_swizzle_data) | ||||
|             throw "Failed to write geomtry shader program binary"; | ||||
| 
 | ||||
|         written = file.WriteArray(gs_swizzle_data.data(), gs_swizzle_data.size()); | ||||
|         if (written != gs_swizzle_data.size() || file.Tell() != initial.gs_float_uniforms) | ||||
|             throw "Failed to write geometry shader swizzle data"; | ||||
| 
 | ||||
|         written = file.WriteArray(gs_float_uniforms.data(), gs_float_uniforms.size()); | ||||
|         if (written != gs_float_uniforms.size() || file.Tell() != initial.gs_float_uniforms + sizeof(u32) * initial.gs_float_uniforms_size) | ||||
|             throw "Failed to write geometry shader float uniforms"; | ||||
| 
 | ||||
|         // Iterate through stream elements, write "extra data"
 | ||||
|         for (const auto& stream_element : stream) { | ||||
|             if (stream_element.extra_data.size() == 0) | ||||
|                 continue; | ||||
| 
 | ||||
|             written = file.WriteBytes(stream_element.extra_data.data(), stream_element.extra_data.size()); | ||||
|             if (written != stream_element.extra_data.size()) | ||||
|                 throw "Failed to write extra data"; | ||||
|         } | ||||
| 
 | ||||
|         if (file.Tell() != header.stream_offset) | ||||
|             throw "Unexpected end of extra data"; | ||||
| 
 | ||||
|         // Write actual stream elements
 | ||||
|         for (const auto& stream_element : stream) { | ||||
|             if (1 != file.WriteObject(stream_element.data)) | ||||
|                 throw "Failed to write stream element"; | ||||
|         } | ||||
|     } catch(const char* str) { | ||||
|         LOG_ERROR(HW_GPU, "Writing CiTrace file failed: %s", str); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Recorder::FrameFinished() { | ||||
|     stream.push_back( { FrameMarker } ); | ||||
| } | ||||
| 
 | ||||
| void Recorder::MemoryAccessed(const u8* data, u32 size, u32 physical_address) { | ||||
|     StreamElement element = { MemoryLoad }; | ||||
|     element.data.memory_load.size = size; | ||||
|     element.data.memory_load.physical_address = physical_address; | ||||
| 
 | ||||
|     // Compute hash over given memory region to check if the contents are already stored internally
 | ||||
|     boost::crc_32_type result; | ||||
|     result.process_bytes(data, size); | ||||
|     element.hash = result.checksum(); | ||||
| 
 | ||||
|     element.uses_existing_data = (memory_regions.find(element.hash) != memory_regions.end()); | ||||
|     if (!element.uses_existing_data) { | ||||
|         element.extra_data.resize(size); | ||||
|         memcpy(element.extra_data.data(), data, size); | ||||
|         memory_regions.insert({element.hash, 0}); // file offset will be initialized in Finish()
 | ||||
|     } | ||||
| 
 | ||||
|     stream.push_back(element); | ||||
| } | ||||
| 
 | ||||
| template<typename T> | ||||
| void Recorder::RegisterWritten(u32 physical_address, T value) { | ||||
|     StreamElement element = { RegisterWrite }; | ||||
|     element.data.register_write.size = (sizeof(T) == 1) ? CTRegisterWrite::SIZE_8 | ||||
|                                      : (sizeof(T) == 2) ? CTRegisterWrite::SIZE_16 | ||||
|                                      : (sizeof(T) == 4) ? CTRegisterWrite::SIZE_32 | ||||
|                                      :                    CTRegisterWrite::SIZE_64; | ||||
|     element.data.register_write.physical_address = physical_address; | ||||
|     element.data.register_write.value = value; | ||||
| 
 | ||||
|     stream.push_back(element); | ||||
| } | ||||
| 
 | ||||
| template void Recorder::RegisterWritten(u32,u8); | ||||
| template void Recorder::RegisterWritten(u32,u16); | ||||
| template void Recorder::RegisterWritten(u32,u32); | ||||
| template void Recorder::RegisterWritten(u32,u64); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/core/tracer/recorder.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/core/tracer/recorder.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| // Copyright 2015 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <unordered_map> | ||||
| #include <vector> | ||||
| 
 | ||||
| #include <boost/crc.hpp> | ||||
| 
 | ||||
| #include "common/common_types.h" | ||||
| 
 | ||||
| #include "tracer.h" | ||||
| 
 | ||||
| namespace CiTrace { | ||||
| 
 | ||||
| class Recorder { | ||||
| public: | ||||
|     /**
 | ||||
|      * Recorder constructor | ||||
|      * @param vs_float_uniforms Pointer to an array of 32-bit-aligned 24-bit floating point values. | ||||
|      */ | ||||
|     Recorder(u32* gpu_registers,     u32 gpu_registers_size, | ||||
|              u32* lcd_registers,     u32 lcd_registers_size, | ||||
|              u32* pica_registers,    u32 pica_registers_size, | ||||
|              u32* vs_program_binary, u32 vs_program_binary_size, | ||||
|              u32* vs_swizzle_data,   u32 vs_swizzle_data_size, | ||||
|              u32* vs_float_uniforms, u32 vs_float_uniforms_size, | ||||
|              u32* gs_program_binary, u32 gs_program_binary_size, | ||||
|              u32* gs_swizzle_data,   u32 gs_swizzle_data_size, | ||||
|              u32* gs_float_uniforms, u32 gs_float_uniforms_size); | ||||
| 
 | ||||
|     /// Finish recording of this Citrace and save it using the given filename.
 | ||||
|     void Finish(const std::string& filename); | ||||
| 
 | ||||
|     /// Mark end of a frame
 | ||||
|     void FrameFinished(); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Store a copy of the given memory range in the recording. | ||||
|      * @note Use this whenever the GPU is about to access a particular memory region. | ||||
|      * @note The implementation will make sure to minimize redundant memory updates. | ||||
|      */ | ||||
|     void MemoryAccessed(const u8* data, u32 size, u32 physical_address); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Record a register write. | ||||
|      * @note Use this whenever a GPU-related MMIO register has been written to. | ||||
|      */ | ||||
|     template<typename T> | ||||
|     void RegisterWritten(u32 physical_address, T value); | ||||
| 
 | ||||
| private: | ||||
|     // Initial state of recording start
 | ||||
|     std::vector<u32> gpu_registers; | ||||
|     std::vector<u32> lcd_registers; | ||||
|     std::vector<u32> pica_registers; | ||||
|     std::vector<u32> vs_program_binary; | ||||
|     std::vector<u32> vs_swizzle_data; | ||||
|     std::vector<u32> vs_float_uniforms; | ||||
|     std::vector<u32> gs_program_binary; | ||||
|     std::vector<u32> gs_swizzle_data; | ||||
|     std::vector<u32> gs_float_uniforms; | ||||
| 
 | ||||
|     // Command stream
 | ||||
|     struct StreamElement { | ||||
|         CTStreamElement data; | ||||
| 
 | ||||
|         /**
 | ||||
|           * Extra data to store along "core" data. | ||||
|           * This is e.g. used for data used in MemoryUpdates. | ||||
|           */ | ||||
|         std::vector<u8> extra_data; | ||||
| 
 | ||||
|         /// Optional CRC hash (e.g. for hashing memory regions)
 | ||||
|         boost::crc_32_type::value_type hash; | ||||
| 
 | ||||
|         /// If true, refer to data already written to the output file instead of extra_data
 | ||||
|         bool uses_existing_data; | ||||
|     }; | ||||
| 
 | ||||
|     std::vector<StreamElement> stream; | ||||
| 
 | ||||
|     /**
 | ||||
|      * Internal cache which maps hashes of memory contents to file offsets at which those memory | ||||
|      * contents are stored. | ||||
|      */ | ||||
|     std::unordered_map<boost::crc_32_type::value_type /*hash*/, u32 /*file_offset*/> memory_regions; | ||||
| }; | ||||
| 
 | ||||
| } // namespace
 | ||||
							
								
								
									
										98
									
								
								src/core/tracer/tracer.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/core/tracer/tracer.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| // Copyright 2015 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <cstdint> | ||||
| 
 | ||||
| namespace CiTrace { | ||||
| 
 | ||||
| // NOTE: Things are stored in little-endian
 | ||||
| 
 | ||||
| #pragma pack(1) | ||||
| 
 | ||||
| struct CTHeader { | ||||
|     static const char* ExpectedMagicWord() { | ||||
|         return "CiTr"; | ||||
|     } | ||||
| 
 | ||||
|     static uint32_t ExpectedVersion() { | ||||
|         return 1; | ||||
|     } | ||||
| 
 | ||||
|     char magic[4]; | ||||
|     uint32_t version; | ||||
|     uint32_t header_size; | ||||
| 
 | ||||
|     struct { | ||||
|         // NOTE: Register range sizes are technically hardware-constants, but the actual limits
 | ||||
|         // aren't known. Hence we store the presumed limits along the offsets.
 | ||||
|         // Sizes are given in uint32_t units.
 | ||||
|         uint32_t gpu_registers; | ||||
|         uint32_t gpu_registers_size; | ||||
|         uint32_t lcd_registers; | ||||
|         uint32_t lcd_registers_size; | ||||
|         uint32_t pica_registers; | ||||
|         uint32_t pica_registers_size; | ||||
|         uint32_t vs_program_binary; | ||||
|         uint32_t vs_program_binary_size; | ||||
|         uint32_t vs_swizzle_data; | ||||
|         uint32_t vs_swizzle_data_size; | ||||
|         uint32_t vs_float_uniforms; | ||||
|         uint32_t vs_float_uniforms_size; | ||||
|         uint32_t gs_program_binary; | ||||
|         uint32_t gs_program_binary_size; | ||||
|         uint32_t gs_swizzle_data; | ||||
|         uint32_t gs_swizzle_data_size; | ||||
|         uint32_t gs_float_uniforms; | ||||
|         uint32_t gs_float_uniforms_size; | ||||
| 
 | ||||
|         // Other things we might want to store here:
 | ||||
|         // - Initial framebuffer data, maybe even a full copy of FCRAM/VRAM
 | ||||
|         // - Default vertex attributes
 | ||||
|     } initial_state_offsets; | ||||
| 
 | ||||
|     uint32_t stream_offset; | ||||
|     uint32_t stream_size; | ||||
| }; | ||||
| 
 | ||||
| enum CTStreamElementType : uint32_t { | ||||
|     FrameMarker   = 0xE1, | ||||
|     MemoryLoad    = 0xE2, | ||||
|     RegisterWrite = 0xE3, | ||||
| }; | ||||
| 
 | ||||
| struct CTMemoryLoad { | ||||
|     uint32_t file_offset; | ||||
|     uint32_t size; | ||||
|     uint32_t physical_address; | ||||
|     uint32_t pad; | ||||
| }; | ||||
| 
 | ||||
| struct CTRegisterWrite { | ||||
|     uint32_t physical_address; | ||||
| 
 | ||||
|     enum : uint32_t { | ||||
|         SIZE_8  = 0xD1, | ||||
|         SIZE_16 = 0xD2, | ||||
|         SIZE_32 = 0xD3, | ||||
|         SIZE_64 = 0xD4 | ||||
|     } size; | ||||
| 
 | ||||
|     // TODO: Make it clearer which bits of this member are used for sizes other than 32 bits
 | ||||
|     uint64_t value; | ||||
| }; | ||||
| 
 | ||||
| struct CTStreamElement { | ||||
|     CTStreamElementType type; | ||||
| 
 | ||||
|     union { | ||||
|         CTMemoryLoad memory_load; | ||||
|         CTRegisterWrite register_write; | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| #pragma pack() | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue