Add interrupt controller emulation
This commit is contained in:
parent
c615e007c0
commit
2128a2984b
|
@ -80,6 +80,18 @@ struct BitField
|
|||
return *this;
|
||||
}
|
||||
|
||||
BitField& operator&=(DataType rhs)
|
||||
{
|
||||
SetValue(GetValue() & rhs);
|
||||
return *this;
|
||||
}
|
||||
|
||||
BitField& operator|=(DataType rhs)
|
||||
{
|
||||
SetValue(GetValue() & rhs);
|
||||
return *this;
|
||||
}
|
||||
|
||||
BitField& operator^=(DataType rhs)
|
||||
{
|
||||
SetValue(GetValue() ^ rhs);
|
||||
|
|
|
@ -3,22 +3,34 @@
|
|||
#include "YBaseLib/Log.h"
|
||||
#include "YBaseLib/String.h"
|
||||
#include "common/state_wrapper.h"
|
||||
#include "cpu_core.h"
|
||||
#include "cpu_disasm.h"
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "interrupt_controller.h"
|
||||
#include <cstdio>
|
||||
Log_SetChannel(Bus);
|
||||
|
||||
// Offset and value remapping for (w32) registers from nocash docs.
|
||||
void FixupUnalignedWordAccessW32(u32& offset, u32& value)
|
||||
{
|
||||
const u32 byte_offset = offset & u32(3);
|
||||
offset &= ~u32(3);
|
||||
value <<= byte_offset * 8;
|
||||
}
|
||||
|
||||
Bus::Bus() = default;
|
||||
|
||||
Bus::~Bus() = default;
|
||||
|
||||
bool Bus::Initialize(System* system, DMA* dma, GPU* gpu)
|
||||
bool Bus::Initialize(CPU::Core* cpu, DMA* dma, InterruptController* interrupt_controller, GPU* gpu)
|
||||
{
|
||||
if (!LoadBIOS())
|
||||
return false;
|
||||
|
||||
m_cpu = cpu;
|
||||
m_dma = dma;
|
||||
m_interrupt_controller = interrupt_controller;
|
||||
m_gpu = gpu;
|
||||
return true;
|
||||
}
|
||||
|
@ -216,6 +228,20 @@ bool Bus::DoWriteGPU(MemoryAccessSize size, u32 offset, u32 value)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Bus::DoReadInterruptController(MemoryAccessSize size, u32 offset, u32& value)
|
||||
{
|
||||
FixupUnalignedWordAccessW32(offset, value);
|
||||
value = m_interrupt_controller->ReadRegister(offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bus::DoWriteInterruptController(MemoryAccessSize size, u32 offset, u32 value)
|
||||
{
|
||||
FixupUnalignedWordAccessW32(offset, value);
|
||||
m_interrupt_controller->WriteRegister(offset, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bus::ReadSPU(MemoryAccessSize size, u32 offset, u32& value)
|
||||
{
|
||||
if (offset == 0x1AE)
|
||||
|
|
|
@ -5,8 +5,15 @@
|
|||
#include <array>
|
||||
|
||||
class StateWrapper;
|
||||
|
||||
namespace CPU
|
||||
{
|
||||
class Core;
|
||||
}
|
||||
|
||||
class DMA;
|
||||
class GPU;
|
||||
class InterruptController;
|
||||
class System;
|
||||
|
||||
class Bus
|
||||
|
@ -15,7 +22,7 @@ public:
|
|||
Bus();
|
||||
~Bus();
|
||||
|
||||
bool Initialize(System* system, DMA* dma, GPU* gpu);
|
||||
bool Initialize(CPU::Core* cpu, DMA* dma, InterruptController* interrupt_controller, GPU* gpu);
|
||||
void Reset();
|
||||
bool DoState(StateWrapper& sw);
|
||||
|
||||
|
@ -32,12 +39,15 @@ public:
|
|||
void PatchBIOS(u32 address, u32 value, u32 mask = UINT32_C(0xFFFFFFFF));
|
||||
|
||||
private:
|
||||
static constexpr u32 DMA_BASE = 0x1F801080;
|
||||
static constexpr u32 DMA_SIZE = 0x80;
|
||||
static constexpr u32 DMA_MASK = DMA_SIZE - 1;
|
||||
static constexpr u32 GPU_BASE = 0x1F801810;
|
||||
static constexpr u32 GPU_SIZE = 0x10;
|
||||
static constexpr u32 GPU_MASK = GPU_SIZE - 1;
|
||||
static constexpr u32 INTERRUPT_CONTROLLER_BASE = 0x1F801070;
|
||||
static constexpr u32 INTERRUPT_CONTROLLER_SIZE = 0x08;
|
||||
static constexpr u32 INTERRUPT_CONTROLLER_MASK = INTERRUPT_CONTROLLER_SIZE - 1;
|
||||
static constexpr u32 DMA_BASE = 0x1F801080;
|
||||
static constexpr u32 DMA_SIZE = 0x80;
|
||||
static constexpr u32 DMA_MASK = DMA_SIZE - 1;
|
||||
static constexpr u32 SPU_BASE = 0x1F801C00;
|
||||
static constexpr u32 SPU_SIZE = 0x300;
|
||||
static constexpr u32 SPU_MASK = 0x3FF;
|
||||
|
@ -64,13 +74,18 @@ private:
|
|||
bool DoReadGPU(MemoryAccessSize size, u32 offset, u32& value);
|
||||
bool DoWriteGPU(MemoryAccessSize size, u32 offset, u32 value);
|
||||
|
||||
bool DoReadInterruptController(MemoryAccessSize size, u32 offset, u32& value);
|
||||
bool DoWriteInterruptController(MemoryAccessSize size, u32 offset, u32 value);
|
||||
|
||||
bool DoReadDMA(MemoryAccessSize size, u32 offset, u32& value);
|
||||
bool DoWriteDMA(MemoryAccessSize size, u32 offset, u32 value);
|
||||
|
||||
bool ReadSPU(MemoryAccessSize size, u32 offset, u32& value);
|
||||
bool WriteSPU(MemoryAccessSize size, u32 offset, u32 value);
|
||||
|
||||
CPU::Core* m_cpu = nullptr;
|
||||
DMA* m_dma = nullptr;
|
||||
InterruptController* m_interrupt_controller = nullptr;
|
||||
GPU* m_gpu = nullptr;
|
||||
|
||||
std::array<u8, 2097152> m_ram{}; // 2MB RAM
|
||||
|
|
|
@ -80,6 +80,16 @@ bool Bus::DispatchAccess(PhysicalMemoryAddress cpu_address, PhysicalMemoryAddres
|
|||
{
|
||||
return DoRAMAccess<type, size>(bus_address, value);
|
||||
}
|
||||
else if (bus_address < INTERRUPT_CONTROLLER_BASE)
|
||||
{
|
||||
return DoInvalidAccess(type, size, cpu_address, bus_address, value);
|
||||
}
|
||||
else if (bus_address < (INTERRUPT_CONTROLLER_BASE + INTERRUPT_CONTROLLER_SIZE))
|
||||
{
|
||||
return (type == MemoryAccessType::Read) ?
|
||||
DoReadInterruptController(size, bus_address & INTERRUPT_CONTROLLER_MASK, value) :
|
||||
DoWriteInterruptController(size, bus_address & INTERRUPT_CONTROLLER_MASK, value);
|
||||
}
|
||||
else if (bus_address < DMA_BASE)
|
||||
{
|
||||
return DoInvalidAccess(type, size, cpu_address, bus_address, value);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
#include "interrupt_controller.h"
|
|
@ -36,6 +36,7 @@ void Core::Reset()
|
|||
m_cop0_regs.BPCM = 0;
|
||||
m_cop0_regs.EPC = 0;
|
||||
m_cop0_regs.sr.bits = 0;
|
||||
m_cop0_regs.cause.bits = 0;
|
||||
|
||||
SetPC(RESET_VECTOR);
|
||||
}
|
||||
|
@ -60,10 +61,13 @@ bool Core::DoState(StateWrapper& sw)
|
|||
sw.Do(&m_cop0_regs.cause.bits);
|
||||
sw.Do(&m_cop0_regs.dcic.bits);
|
||||
sw.Do(&m_next_instruction.bits);
|
||||
sw.Do(&m_in_branch_delay_slot);
|
||||
sw.Do(&m_branched);
|
||||
sw.Do(&m_current_instruction_pc);
|
||||
sw.Do(&m_load_delay_reg);
|
||||
sw.Do(&m_load_delay_old_value);
|
||||
sw.Do(&m_next_load_delay_reg);
|
||||
sw.Do(&m_next_load_delay_old_value);
|
||||
sw.Do(&m_in_branch_delay_slot);
|
||||
sw.Do(&m_branched);
|
||||
sw.Do(&m_cache_control);
|
||||
sw.DoBytes(m_dcache.data(), m_dcache.size());
|
||||
return !sw.HasError();
|
||||
|
@ -197,6 +201,28 @@ void Core::RaiseException(Exception excode, u8 coprocessor /* = 0 */)
|
|||
FlushPipeline();
|
||||
}
|
||||
|
||||
void Core::SetExternalInterrupt(u8 bit)
|
||||
{
|
||||
m_cop0_regs.cause.Ip |= static_cast<u8>(1u << bit);
|
||||
}
|
||||
|
||||
void Core::ClearExternalInterrupt(u8 bit)
|
||||
{
|
||||
m_cop0_regs.cause.Ip &= static_cast<u8>(~(1u << bit));
|
||||
}
|
||||
|
||||
bool Core::DispatchInterrupts()
|
||||
{
|
||||
// const bool do_interrupt = m_cop0_regs.sr.IEc && ((m_cop0_regs.cause.Ip & m_cop0_regs.sr.Im) != 0);
|
||||
const bool do_interrupt =
|
||||
m_cop0_regs.sr.IEc && (((m_cop0_regs.cause.bits & m_cop0_regs.sr.bits) & (UINT32_C(0xFF) << 8)) != 0);
|
||||
if (!do_interrupt)
|
||||
return false;
|
||||
|
||||
RaiseException(Exception::INT);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Core::FlushLoadDelay()
|
||||
{
|
||||
m_load_delay_reg = Reg::count;
|
||||
|
@ -284,7 +310,7 @@ TickCount Core::Execute()
|
|||
m_current_instruction_pc = m_regs.pc;
|
||||
|
||||
// fetch the next instruction
|
||||
if (!FetchInstruction())
|
||||
if (DispatchInterrupts() || !FetchInstruction())
|
||||
continue;
|
||||
|
||||
// handle branch delays - we are now in a delay slot if we just branched
|
||||
|
@ -300,7 +326,7 @@ TickCount Core::Execute()
|
|||
m_load_delay_old_value = m_next_load_delay_old_value;
|
||||
m_next_load_delay_old_value = 0;
|
||||
}
|
||||
|
||||
|
||||
// reset slice ticks, it'll be updated when the components execute
|
||||
m_slice_ticks = MAX_CPU_SLICE_SIZE;
|
||||
return executed_ticks;
|
||||
|
|
|
@ -28,14 +28,11 @@ public:
|
|||
|
||||
TickCount Execute();
|
||||
|
||||
void SetSliceTicks(TickCount downcount)
|
||||
{
|
||||
m_slice_ticks = (downcount < m_slice_ticks ? downcount : m_slice_ticks);
|
||||
}
|
||||
|
||||
const Registers& GetRegs() const { return m_regs; }
|
||||
Registers& GetRegs() { return m_regs; }
|
||||
|
||||
void SetSliceTicks(TickCount downcount) { m_slice_ticks = (downcount < m_slice_ticks ? downcount : m_slice_ticks); }
|
||||
|
||||
// Sets the PC and flushes the pipeline.
|
||||
void SetPC(u32 new_pc);
|
||||
|
||||
|
@ -46,6 +43,10 @@ public:
|
|||
bool SafeWriteMemoryHalfWord(VirtualMemoryAddress addr, u16 value);
|
||||
bool SafeWriteMemoryWord(VirtualMemoryAddress addr, u32 value);
|
||||
|
||||
// External IRQs
|
||||
void SetExternalInterrupt(u8 bit);
|
||||
void ClearExternalInterrupt(u8 bit);
|
||||
|
||||
private:
|
||||
template<MemoryAccessType type, MemoryAccessSize size, bool is_instruction_fetch, bool raise_exceptions>
|
||||
bool DoMemoryAccess(VirtualMemoryAddress address, u32& value);
|
||||
|
@ -78,6 +79,7 @@ private:
|
|||
// exceptions
|
||||
u32 GetExceptionVector(Exception excode) const;
|
||||
void RaiseException(Exception excode, u8 coprocessor = 0);
|
||||
bool DispatchInterrupts();
|
||||
|
||||
// flushes any load delays if present
|
||||
void FlushLoadDelay();
|
||||
|
@ -101,9 +103,8 @@ private:
|
|||
TickCount m_slice_ticks = 0;
|
||||
|
||||
Registers m_regs = {};
|
||||
Cop0Registers m_cop0_regs = {};
|
||||
Instruction m_next_instruction = {};
|
||||
bool m_in_branch_delay_slot = false;
|
||||
bool m_branched = false;
|
||||
|
||||
// address of the instruction currently being executed
|
||||
u32 m_current_instruction_pc = 0;
|
||||
|
@ -113,11 +114,11 @@ private:
|
|||
u32 m_load_delay_old_value = 0;
|
||||
Reg m_next_load_delay_reg = Reg::count;
|
||||
u32 m_next_load_delay_old_value = 0;
|
||||
bool m_in_branch_delay_slot = false;
|
||||
bool m_branched = false;
|
||||
|
||||
u32 m_cache_control = 0;
|
||||
|
||||
Cop0Registers m_cop0_regs = {};
|
||||
|
||||
// data cache (used as scratchpad)
|
||||
std::array<u8, DCACHE_SIZE> m_dcache = {};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
#include "interrupt_controller.h"
|
||||
#include "YBaseLib/Log.h"
|
||||
#include "common/state_wrapper.h"
|
||||
#include "cpu_core.h"
|
||||
Log_SetChannel(InterruptController);
|
||||
|
||||
InterruptController::InterruptController() = default;
|
||||
|
||||
InterruptController::~InterruptController() = default;
|
||||
|
||||
bool InterruptController::Initialize(CPU::Core* cpu)
|
||||
{
|
||||
m_cpu = cpu;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InterruptController::Reset()
|
||||
{
|
||||
m_interrupt_status_register = 0;
|
||||
m_interrupt_mask_register = DEFAULT_INTERRUPT_MASK;
|
||||
}
|
||||
|
||||
bool InterruptController::DoState(StateWrapper& sw)
|
||||
{
|
||||
sw.Do(&m_interrupt_status_register);
|
||||
sw.Do(&m_interrupt_mask_register);
|
||||
|
||||
return !sw.HasError();
|
||||
}
|
||||
|
||||
void InterruptController::InterruptRequest(IRQ irq)
|
||||
{
|
||||
const u32 bit = (u32(1) << static_cast<u32>(irq));
|
||||
m_interrupt_status_register |= (bit & m_interrupt_mask_register);
|
||||
UpdateCPUInterruptRequest();
|
||||
}
|
||||
|
||||
u32 InterruptController::ReadRegister(u32 offset)
|
||||
{
|
||||
switch (offset)
|
||||
{
|
||||
case 0x00: // I_STATUS
|
||||
return m_interrupt_status_register;
|
||||
|
||||
case 0x04: // I_MASK
|
||||
return m_interrupt_mask_register;
|
||||
|
||||
default:
|
||||
Log_ErrorPrintf("Invalid read at offset 0x%08X", offset);
|
||||
return UINT32_C(0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
void InterruptController::WriteRegister(u32 offset, u32 value)
|
||||
{
|
||||
switch (offset)
|
||||
{
|
||||
case 0x00: // I_STATUS
|
||||
{
|
||||
Log_DebugPrintf("Clearing bits 0x%08X", value);
|
||||
m_interrupt_status_register = m_interrupt_status_register & (~(value & REGISTER_WRITE_MASK));
|
||||
UpdateCPUInterruptRequest();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x04: // I_MASK
|
||||
{
|
||||
Log_DebugPrintf("Interrupt mask <- 0x%08X", value);
|
||||
m_interrupt_mask_register = value & REGISTER_WRITE_MASK;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log_ErrorPrintf("Invalid write at offset 0x%08X", offset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void InterruptController::UpdateCPUInterruptRequest()
|
||||
{
|
||||
// external interrupts set bit 10 only?
|
||||
if (m_interrupt_status_register != 0)
|
||||
m_cpu->SetExternalInterrupt(3);
|
||||
else
|
||||
m_cpu->ClearExternalInterrupt(3);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
|
||||
class StateWrapper;
|
||||
|
||||
namespace CPU
|
||||
{
|
||||
class Core;
|
||||
}
|
||||
|
||||
class InterruptController
|
||||
{
|
||||
public:
|
||||
static constexpr u32 NUM_IRQS = 11;
|
||||
|
||||
enum class IRQ : u32
|
||||
{
|
||||
VBLANK = 0, // IRQ0 - VBLANK
|
||||
GPU = 1, // IRQ1 - GPU via GP0(1Fh)
|
||||
CDROM = 2, // IRQ2 - CDROM
|
||||
DMA = 3, // IRQ3 - DMA
|
||||
TMR0 = 4, // IRQ4 - TMR0 - Sysclk or Dotclk
|
||||
TMR1 = 5, // IRQ5 - TMR1 - Sysclk Hblank
|
||||
TMR2 = 6, // IRQ6 - TMR2 - Sysclk or Sysclk / 8
|
||||
IRQ7 = 7, // IRQ7 - Controller and Memory Card Byte Received
|
||||
SIO = 8, // IRQ8 - SIO
|
||||
SPU = 9, // IRQ9 - SPU
|
||||
IRQ10 = 10 // IRQ10 - Lightpen interrupt, PIO
|
||||
};
|
||||
|
||||
|
||||
InterruptController();
|
||||
~InterruptController();
|
||||
|
||||
bool Initialize(CPU::Core* cpu);
|
||||
void Reset();
|
||||
bool DoState(StateWrapper& sw);
|
||||
|
||||
// Should mirror CPU state.
|
||||
bool GetIRQLineState() const { return (m_interrupt_status_register != 0); }
|
||||
|
||||
// Interupts are edge-triggered, so if it is masked when TriggerInterrupt() is called, it will be lost.
|
||||
void InterruptRequest(IRQ irq);
|
||||
|
||||
// I/O
|
||||
u32 ReadRegister(u32 offset);
|
||||
void WriteRegister(u32 offset, u32 value);
|
||||
|
||||
private:
|
||||
static constexpr u32 REGISTER_WRITE_MASK = (u32(1) << NUM_IRQS) - 1;
|
||||
static constexpr u32 DEFAULT_INTERRUPT_MASK = (u32(1) << NUM_IRQS) - 1;
|
||||
|
||||
void UpdateCPUInterruptRequest();
|
||||
|
||||
CPU::Core* m_cpu;
|
||||
|
||||
u32 m_interrupt_status_register = 0;
|
||||
u32 m_interrupt_mask_register = DEFAULT_INTERRUPT_MASK;
|
||||
};
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
<ClCompile Include="gpu_hw.cpp" />
|
||||
<ClCompile Include="gpu_hw_opengl.cpp" />
|
||||
<ClCompile Include="host_interface.cpp" />
|
||||
<ClCompile Include="interrupt_controller.cpp" />
|
||||
<ClCompile Include="system.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -55,6 +56,7 @@
|
|||
<ClInclude Include="gpu_hw.h" />
|
||||
<ClInclude Include="gpu_hw_opengl.h" />
|
||||
<ClInclude Include="host_interface.h" />
|
||||
<ClInclude Include="interrupt_controller.h" />
|
||||
<ClInclude Include="save_state_version.h" />
|
||||
<ClInclude Include="system.h" />
|
||||
<ClInclude Include="types.h" />
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<ClCompile Include="gpu_hw_opengl.cpp" />
|
||||
<ClCompile Include="gpu_hw.cpp" />
|
||||
<ClCompile Include="host_interface.cpp" />
|
||||
<ClCompile Include="interrupt_controller.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="types.h" />
|
||||
|
@ -24,6 +25,7 @@
|
|||
<ClInclude Include="gpu_hw_opengl.h" />
|
||||
<ClInclude Include="gpu_hw.h" />
|
||||
<ClInclude Include="host_interface.h" />
|
||||
<ClInclude Include="interrupt_controller.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="cpu_core.inl" />
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
#include "cpu_core.h"
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "interrupt_controller.h"
|
||||
|
||||
System::System(HostInterface* host_interface) : m_host_interface(host_interface)
|
||||
{
|
||||
m_cpu = std::make_unique<CPU::Core>();
|
||||
m_bus = std::make_unique<Bus>();
|
||||
m_dma = std::make_unique<DMA>();
|
||||
m_interrupt_controller = std::make_unique<InterruptController>();
|
||||
// m_gpu = std::make_unique<GPU>();
|
||||
m_gpu = GPU::CreateHardwareOpenGLRenderer();
|
||||
}
|
||||
|
@ -22,12 +24,15 @@ bool System::Initialize()
|
|||
if (!m_cpu->Initialize(m_bus.get()))
|
||||
return false;
|
||||
|
||||
if (!m_bus->Initialize(this, m_dma.get(), m_gpu.get()))
|
||||
if (!m_bus->Initialize(m_cpu.get(), m_dma.get(), m_interrupt_controller.get(), m_gpu.get()))
|
||||
return false;
|
||||
|
||||
if (!m_dma->Initialize(m_bus.get(), m_gpu.get()))
|
||||
return false;
|
||||
|
||||
if (!m_interrupt_controller->Initialize(m_cpu.get()))
|
||||
return false;
|
||||
|
||||
if (!m_gpu->Initialize(this, m_bus.get(), m_dma.get()))
|
||||
return false;
|
||||
|
||||
|
@ -45,6 +50,9 @@ bool System::DoState(StateWrapper& sw)
|
|||
if (!sw.DoMarker("DMA") || !m_dma->DoState(sw))
|
||||
return false;
|
||||
|
||||
if (!sw.DoMarker("InterruptController") || !m_interrupt_controller->DoState(sw))
|
||||
return false;
|
||||
|
||||
if (!sw.DoMarker("GPU") || !m_gpu->DoState(sw))
|
||||
return false;
|
||||
|
||||
|
@ -58,6 +66,7 @@ void System::Reset()
|
|||
m_cpu->Reset();
|
||||
m_bus->Reset();
|
||||
m_dma->Reset();
|
||||
m_interrupt_controller->Reset();
|
||||
m_gpu->Reset();
|
||||
m_frame_number = 1;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ class Core;
|
|||
|
||||
class Bus;
|
||||
class DMA;
|
||||
class InterruptController;
|
||||
class GPU;
|
||||
|
||||
class System
|
||||
|
@ -45,6 +46,7 @@ private:
|
|||
std::unique_ptr<CPU::Core> m_cpu;
|
||||
std::unique_ptr<Bus> m_bus;
|
||||
std::unique_ptr<DMA> m_dma;
|
||||
std::unique_ptr<InterruptController> m_interrupt_controller;
|
||||
std::unique_ptr<GPU> m_gpu;
|
||||
u32 m_frame_number = 1;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue