diff options
Diffstat (limited to '')
-rw-r--r-- | src/citra_qt/debugger/callstack.cpp | 3 | ||||
-rw-r--r-- | src/common/thread_queue_list.h | 218 | ||||
-rw-r--r-- | src/core/arm/dyncom/arm_dyncom.cpp | 6 | ||||
-rw-r--r-- | src/core/arm/dyncom/arm_dyncom.h | 3 | ||||
-rw-r--r-- | src/core/arm/dyncom/arm_dyncom_interpreter.cpp | 25 | ||||
-rw-r--r-- | src/core/core.cpp | 19 | ||||
-rw-r--r-- | src/core/hle/kernel/kernel.cpp | 2 | ||||
-rw-r--r-- | src/core/hle/kernel/thread.cpp | 63 | ||||
-rw-r--r-- | src/core/hle/kernel/thread.h | 18 | ||||
-rw-r--r-- | src/core/hle/service/gsp_gpu.cpp | 5 | ||||
-rw-r--r-- | src/core/hle/svc.cpp | 4 | ||||
-rw-r--r-- | src/core/system.cpp | 4 | ||||
-rw-r--r-- | src/video_core/renderer_opengl/renderer_opengl.cpp | 5 |
13 files changed, 191 insertions, 184 deletions
diff --git a/src/citra_qt/debugger/callstack.cpp b/src/citra_qt/debugger/callstack.cpp index bcc5d2143..274c5cccd 100644 --- a/src/citra_qt/debugger/callstack.cpp +++ b/src/citra_qt/debugger/callstack.cpp @@ -38,6 +38,9 @@ void CallstackWidget::OnCPUStepped() { ret_addr = Memory::Read32(addr); call_addr = ret_addr - 4; //get call address??? + + if (Memory::GetPointer(call_addr) == nullptr) + break; /* TODO (mattvail) clean me, move to debugger interface */ u32 insn = Memory::Read32(call_addr); diff --git a/src/common/thread_queue_list.h b/src/common/thread_queue_list.h index 4e1c0a215..444abf115 100644 --- a/src/common/thread_queue_list.h +++ b/src/common/thread_queue_list.h @@ -4,213 +4,143 @@ #pragma once +#include <array> +#include <deque> + +#include <boost/range/algorithm_ext/erase.hpp> + #include "common/common.h" namespace Common { -template<class IdType> +template<class T, unsigned int N> struct ThreadQueueList { - // Number of queues (number of priority levels starting at 0.) - static const int NUM_QUEUES = 128; + // TODO(yuriks): If performance proves to be a problem, the std::deques can be replaced with + // (dynamically resizable) circular buffers to remove their overhead when + // inserting and popping. - // Initial number of threads a single queue can handle. - static const int INITIAL_CAPACITY = 32; + typedef unsigned int Priority; - struct Queue { - // Next ever-been-used queue (worse priority.) - Queue *next; - // First valid item in data. - int first; - // One after last valid item in data. - int end; - // A too-large array with room on the front and end. - IdType *data; - // Size of data array. - int capacity; - }; + // Number of priority levels. (Valid levels are [0..NUM_QUEUES).) + static const Priority NUM_QUEUES = N; ThreadQueueList() { - memset(queues, 0, sizeof(queues)); - first = invalid(); - } - - ~ThreadQueueList() { - for (int i = 0; i < NUM_QUEUES; ++i) - { - if (queues[i].data != nullptr) - free(queues[i].data); - } + first = nullptr; } // Only for debugging, returns priority level. - int contains(const IdType uid) { - for (int i = 0; i < NUM_QUEUES; ++i) - { - if (queues[i].data == nullptr) - continue; - - Queue *cur = &queues[i]; - for (int j = cur->first; j < cur->end; ++j) - { - if (cur->data[j] == uid) - return i; + Priority contains(const T& uid) { + for (Priority i = 0; i < NUM_QUEUES; ++i) { + Queue& cur = queues[i]; + if (std::find(cur.data.cbegin(), cur.data.cend(), uid) != cur.data.cend()) { + return i; } } return -1; } - inline IdType pop_first() { + T pop_first() { Queue *cur = first; - while (cur != invalid()) - { - if (cur->end - cur->first > 0) - return cur->data[cur->first++]; - cur = cur->next; + while (cur != nullptr) { + if (!cur->data.empty()) { + auto tmp = std::move(cur->data.front()); + cur->data.pop_front(); + return tmp; + } + cur = cur->next_nonempty; } - //_dbg_assert_msg_(SCEKERNEL, false, "ThreadQueueList should not be empty."); - return 0; + return T(); } - inline IdType pop_first_better(u32 priority) { + T pop_first_better(Priority priority) { Queue *cur = first; Queue *stop = &queues[priority]; - while (cur < stop) - { - if (cur->end - cur->first > 0) - return cur->data[cur->first++]; - cur = cur->next; + while (cur < stop) { + if (!cur->data.empty()) { + auto tmp = std::move(cur->data.front()); + cur->data.pop_front(); + return tmp; + } + cur = cur->next_nonempty; } - return 0; + return T(); } - inline void push_front(u32 priority, const IdType threadID) { + void push_front(Priority priority, const T& thread_id) { Queue *cur = &queues[priority]; - cur->data[--cur->first] = threadID; - if (cur->first == 0) - rebalance(priority); + cur->data.push_front(thread_id); } - inline void push_back(u32 priority, const IdType threadID) { + void push_back(Priority priority, const T& thread_id) { Queue *cur = &queues[priority]; - cur->data[cur->end++] = threadID; - if (cur->end == cur->capacity) - rebalance(priority); + cur->data.push_back(thread_id); } - inline void remove(u32 priority, const IdType threadID) { + void remove(Priority priority, const T& thread_id) { Queue *cur = &queues[priority]; - //_dbg_assert_msg_(SCEKERNEL, cur->next != NULL, "ThreadQueueList::Queue should already be linked up."); - - for (int i = cur->first; i < cur->end; ++i) - { - if (cur->data[i] == threadID) - { - int remaining = --cur->end - i; - if (remaining > 0) - memmove(&cur->data[i], &cur->data[i + 1], remaining * sizeof(IdType)); - return; - } - } - - // Wasn't there. + boost::remove_erase(cur->data, thread_id); } - inline void rotate(u32 priority) { + void rotate(Priority priority) { Queue *cur = &queues[priority]; - //_dbg_assert_msg_(SCEKERNEL, cur->next != NULL, "ThreadQueueList::Queue should already be linked up."); - if (cur->end - cur->first > 1) - { - cur->data[cur->end++] = cur->data[cur->first++]; - if (cur->end == cur->capacity) - rebalance(priority); + if (cur->data.size() > 1) { + cur->data.push_back(std::move(cur->data.front())); + cur->data.pop_front(); } } - inline void clear() { - for (int i = 0; i < NUM_QUEUES; ++i) - { - if (queues[i].data != nullptr) - free(queues[i].data); - } - memset(queues, 0, sizeof(queues)); - first = invalid(); + void clear() { + queues.fill(Queue()); + first = nullptr; } - inline bool empty(u32 priority) const { + bool empty(Priority priority) const { const Queue *cur = &queues[priority]; - return cur->first == cur->end; + return cur->data.empty(); } - inline void prepare(u32 priority) { - Queue *cur = &queues[priority]; - if (cur->next == nullptr) - link(priority, INITIAL_CAPACITY); + void prepare(Priority priority) { + Queue* cur = &queues[priority]; + if (cur->next_nonempty == UnlinkedTag()) + link(priority); } private: - Queue *invalid() const { - return (Queue *) -1; + struct Queue { + // Points to the next active priority, skipping over ones that have never been used. + Queue* next_nonempty = UnlinkedTag(); + // Double-ended queue of threads in this priority level + std::deque<T> data; + }; + + /// Special tag used to mark priority levels that have never been used. + static Queue* UnlinkedTag() { + return reinterpret_cast<Queue*>(1); } - void link(u32 priority, int size) { - //_dbg_assert_msg_(SCEKERNEL, queues[priority].data == NULL, "ThreadQueueList::Queue should only be initialized once."); - - if (size <= INITIAL_CAPACITY) - size = INITIAL_CAPACITY; - else - { - int goal = size; - size = INITIAL_CAPACITY; - while (size < goal) - size *= 2; - } + void link(Priority priority) { Queue *cur = &queues[priority]; - cur->data = (IdType *) malloc(sizeof(IdType) * size); - cur->capacity = size; - cur->first = size / 2; - cur->end = size / 2; - - for (int i = (int) priority - 1; i >= 0; --i) - { - if (queues[i].next != nullptr) - { - cur->next = queues[i].next; - queues[i].next = cur; + + for (int i = priority - 1; i >= 0; --i) { + if (queues[i].next_nonempty != UnlinkedTag()) { + cur->next_nonempty = queues[i].next_nonempty; + queues[i].next_nonempty = cur; return; } } - cur->next = first; + cur->next_nonempty = first; first = cur; } - void rebalance(u32 priority) { - Queue *cur = &queues[priority]; - int size = cur->end - cur->first; - if (size >= cur->capacity - 2) { - IdType *new_data = (IdType *)realloc(cur->data, cur->capacity * 2 * sizeof(IdType)); - if (new_data != nullptr) { - cur->capacity *= 2; - cur->data = new_data; - } - } - - int newFirst = (cur->capacity - size) / 2; - if (newFirst != cur->first) { - memmove(&cur->data[newFirst], &cur->data[cur->first], size * sizeof(IdType)); - cur->first = newFirst; - cur->end = newFirst + size; - } - } - // The first queue that's ever been used. - Queue *first; + Queue* first; // The priority level queues of thread ids. - Queue queues[NUM_QUEUES]; + std::array<Queue, NUM_QUEUES> queues; }; } // namespace diff --git a/src/core/arm/dyncom/arm_dyncom.cpp b/src/core/arm/dyncom/arm_dyncom.cpp index c779e3fd4..31eb879a2 100644 --- a/src/core/arm/dyncom/arm_dyncom.cpp +++ b/src/core/arm/dyncom/arm_dyncom.cpp @@ -15,7 +15,7 @@ const static cpu_config_t s_arm11_cpu_info = { "armv6", "arm11", 0x0007b000, 0x0007f000, NONCACHE }; -ARM_DynCom::ARM_DynCom() : ticks(0) { +ARM_DynCom::ARM_DynCom() { state = std::unique_ptr<ARMul_State>(new ARMul_State); ARMul_EmulateInit(); @@ -74,11 +74,11 @@ void ARM_DynCom::SetCPSR(u32 cpsr) { } u64 ARM_DynCom::GetTicks() const { - return ticks; + // TODO(Subv): Remove ARM_DynCom::GetTicks() and use CoreTiming::GetTicks() directly once ARMemu is gone + return CoreTiming::GetTicks(); } void ARM_DynCom::AddTicks(u64 ticks) { - this->ticks += ticks; down_count -= ticks; if (down_count < 0) CoreTiming::Advance(); diff --git a/src/core/arm/dyncom/arm_dyncom.h b/src/core/arm/dyncom/arm_dyncom.h index 7284dcd07..9e102a46e 100644 --- a/src/core/arm/dyncom/arm_dyncom.h +++ b/src/core/arm/dyncom/arm_dyncom.h @@ -89,8 +89,5 @@ public: void ExecuteInstructions(int num_instructions) override; private: - std::unique_ptr<ARMul_State> state; - u64 ticks; - }; diff --git a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp index 426fc6474..e3ca02e98 100644 --- a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp +++ b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp @@ -5891,16 +5891,13 @@ unsigned InterpreterMainLoop(ARMul_State* state) { SMULW_INST: { - if ((inst_base->cond == 0xe) || CondPassed(cpu, inst_base->cond)) { - smlad_inst *inst_cream = (smlad_inst *)inst_base->component; - int64_t rm = RM; - int64_t rn = RN; - if (inst_cream->m) - rm = BITS(rm, 16, 31); - else - rm = BITS(rm, 0, 15); - int64_t rst = rm * rn; - RD = BITS(rst, 16, 47); + if (inst_base->cond == 0xE || CondPassed(cpu, inst_base->cond)) { + smlad_inst* const inst_cream = (smlad_inst*)inst_base->component; + + s16 rm = (inst_cream->m == 1) ? ((RM >> 16) & 0xFFFF) : (RM & 0xFFFF); + + s64 result = (s64)rm * (s64)(s32)RN; + RD = BITS(result, 16, 47); } cpu->Reg[15] += GET_INST_SIZE(cpu); INC_PC(sizeof(smlad_inst)); @@ -6699,10 +6696,10 @@ unsigned InterpreterMainLoop(ARMul_State* state) { { if (inst_base->cond == 0xE || CondPassed(cpu, inst_base->cond)) { umaal_inst* const inst_cream = (umaal_inst*)inst_base->component; - const u32 rm = RM; - const u32 rn = RN; - const u32 rd_lo = RDLO; - const u32 rd_hi = RDHI; + const u64 rm = RM; + const u64 rn = RN; + const u64 rd_lo = RDLO; + const u64 rd_hi = RDHI; const u64 result = (rm * rn) + rd_lo + rd_hi; RDLO = (result & 0xFFFFFFFF); diff --git a/src/core/core.cpp b/src/core/core.cpp index 8ac4481cc..ff506d67d 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -5,6 +5,7 @@ #include "common/common_types.h" #include "core/core.h" +#include "core/core_timing.h" #include "core/settings.h" #include "core/arm/disassembler/arm_disasm.h" @@ -16,14 +17,22 @@ namespace Core { -static u64 last_ticks = 0; ///< Last CPU ticks -static ARM_Disasm* disasm = nullptr; ///< ARM disassembler ARM_Interface* g_app_core = nullptr; ///< ARM11 application core ARM_Interface* g_sys_core = nullptr; ///< ARM11 system (OS) core /// Run the core CPU loop void RunLoop(int tight_loop) { - g_app_core->Run(tight_loop); + // If the current thread is an idle thread, then don't execute instructions, + // instead advance to the next event and try to yield to the next thread + if (Kernel::IsIdleThread(Kernel::GetCurrentThreadHandle())) { + LOG_TRACE(Core_ARM11, "Idling"); + CoreTiming::Idle(); + CoreTiming::Advance(); + HLE::Reschedule(__func__); + } else { + g_app_core->Run(tight_loop); + } + HW::Update(); if (HLE::g_reschedule) { Kernel::Reschedule(); @@ -49,7 +58,6 @@ void Stop() { int Init() { LOG_DEBUG(Core, "initialized OK"); - disasm = new ARM_Disasm(); g_sys_core = new ARM_Interpreter(); switch (Settings::values.cpu_core) { @@ -62,13 +70,10 @@ int Init() { break; } - last_ticks = Core::g_app_core->GetTicks(); - return 0; } void Shutdown() { - delete disasm; delete g_app_core; delete g_sys_core; diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 084fd03ae..391e833c0 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -126,6 +126,8 @@ bool LoadExec(u32 entry_point) { // 0x30 is the typical main thread priority I've seen used so far g_main_thread = Kernel::SetupMainThread(0x30); + // Setup the idle thread + Kernel::SetupIdleThread(); return true; } diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 872df2d14..954bd09a0 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -11,6 +11,7 @@ #include "common/thread_queue_list.h" #include "core/core.h" +#include "core/core_timing.h" #include "core/hle/hle.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/thread.h" @@ -34,6 +35,7 @@ public: inline bool IsReady() const { return (status & THREADSTATUS_READY) != 0; } inline bool IsWaiting() const { return (status & THREADSTATUS_WAIT) != 0; } inline bool IsSuspended() const { return (status & THREADSTATUS_SUSPEND) != 0; } + inline bool IsIdle() const { return idle; } ResultVal<bool> WaitSynchronization() override { const bool wait = status != THREADSTATUS_DORMANT; @@ -69,13 +71,16 @@ public: std::vector<Handle> waiting_threads; std::string name; + + /// Whether this thread is intended to never actually be executed, i.e. always idle + bool idle = false; }; // Lists all thread ids that aren't deleted/etc. static std::vector<Handle> thread_queue; // Lists only ready thread ids. -static Common::ThreadQueueList<Handle> thread_ready_queue; +static Common::ThreadQueueList<Handle, THREADPRIO_LOWEST+1> thread_ready_queue; static Handle current_thread_handle; static Thread* current_thread; @@ -303,6 +308,37 @@ void WaitCurrentThread(WaitType wait_type, Handle wait_handle, VAddr wait_addres GetCurrentThread()->wait_address = wait_address; } +/// Event type for the thread wake up event +static int ThreadWakeupEventType = -1; + +/// Callback that will wake up the thread it was scheduled for +static void ThreadWakeupCallback(u64 parameter, int cycles_late) { + Handle handle = static_cast<Handle>(parameter); + Thread* thread = Kernel::g_handle_table.Get<Thread>(handle); + if (thread == nullptr) { + LOG_ERROR(Kernel, "Thread doesn't exist %u", handle); + return; + } + + Kernel::ResumeThreadFromWait(handle); +} + + +void WakeThreadAfterDelay(Handle handle, s64 nanoseconds) { + // Don't schedule a wakeup if the thread wants to wait forever + if (nanoseconds == -1) + return; + + Thread* thread = Kernel::g_handle_table.Get<Thread>(handle); + if (thread == nullptr) { + LOG_ERROR(Kernel, "Thread doesn't exist %u", handle); + return; + } + + u64 microseconds = nanoseconds / 1000; + CoreTiming::ScheduleEvent(usToCycles(microseconds), ThreadWakeupEventType, handle); +} + /// Resumes a thread from waiting by marking it as "ready" void ResumeThreadFromWait(Handle handle) { Thread* thread = Kernel::g_handle_table.Get<Thread>(handle); @@ -444,7 +480,14 @@ ResultCode SetThreadPriority(Handle handle, s32 priority) { return RESULT_SUCCESS; } -/// Sets up the primary application thread +Handle SetupIdleThread() { + Handle handle; + Thread* thread = CreateThread(handle, "idle", 0, THREADPRIO_LOWEST, THREADPROCESSORID_0, 0, 0); + thread->idle = true; + CallThread(thread); + return handle; +} + Handle SetupMainThread(s32 priority, int stack_size) { Handle handle; @@ -487,14 +530,15 @@ void Reschedule() { thread->GetHandle(), thread->current_priority, thread->status, thread->wait_type, thread->wait_handle); } } +} - // TODO(bunnei): Hack - There is no timing mechanism yet to wake up a thread if it has been put - // to sleep. So, we'll just immediately set it to "ready" again after an attempted context - // switch has occurred. This results in the current thread yielding on a sleep once, and then it - // will immediately be placed back in the queue for execution. - - if (CheckWaitType(prev, WAITTYPE_SLEEP)) - ResumeThreadFromWait(prev->GetHandle()); +bool IsIdleThread(Handle handle) { + Thread* thread = g_handle_table.Get<Thread>(handle); + if (!thread) { + LOG_ERROR(Kernel, "Thread not found %u", handle); + return false; + } + return thread->IsIdle(); } ResultCode GetThreadId(u32* thread_id, Handle handle) { @@ -512,6 +556,7 @@ ResultCode GetThreadId(u32* thread_id, Handle handle) { void ThreadingInit() { next_thread_id = INITIAL_THREAD_ID; + ThreadWakeupEventType = CoreTiming::RegisterEvent("ThreadWakeupCallback", ThreadWakeupCallback); } void ThreadingShutdown() { diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index 81736a866..58bd85ac6 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -89,6 +89,13 @@ Handle GetCurrentThreadHandle(); void WaitCurrentThread(WaitType wait_type, Handle wait_handle=GetCurrentThreadHandle()); /** + * Schedules an event to wake up the specified thread after the specified delay. + * @param handle The thread handle. + * @param nanoseconds The time this thread will be allowed to sleep for. + */ +void WakeThreadAfterDelay(Handle handle, s64 nanoseconds); + +/** * Puts the current thread in the wait state for the given type * @param wait_type Type of wait * @param wait_handle Handle of Kernel object that we are waiting on, defaults to current thread @@ -105,6 +112,17 @@ ResultVal<u32> GetThreadPriority(const Handle handle); /// Set the priority of the thread specified by handle ResultCode SetThreadPriority(Handle handle, s32 priority); +/** + * Sets up the idle thread, this is a thread that is intended to never execute instructions, + * only to advance the timing. It is scheduled when there are no other ready threads in the thread queue + * and will try to yield on every call. + * @returns The handle of the idle thread + */ +Handle SetupIdleThread(); + +/// Whether the current thread is an idle thread +bool IsIdleThread(Handle thread); + /// Initialize threading void ThreadingInit(); diff --git a/src/core/hle/service/gsp_gpu.cpp b/src/core/hle/service/gsp_gpu.cpp index 0127d4ee5..26a43217e 100644 --- a/src/core/hle/service/gsp_gpu.cpp +++ b/src/core/hle/service/gsp_gpu.cpp @@ -291,8 +291,11 @@ static void ExecuteCommand(const Command& command, u32 thread_id) { // Update framebuffer information if requested for (int screen_id = 0; screen_id < 2; ++screen_id) { FrameBufferUpdate* info = GetFrameBufferInfo(thread_id, screen_id); - if (info->is_dirty) + + if (info->is_dirty) { SetBufferSwap(screen_id, info->framebuffer_info[info->index]); + info->framebuffer_info->active_fb = info->framebuffer_info->active_fb ^ 1; + } info->is_dirty = false; } diff --git a/src/core/hle/svc.cpp b/src/core/hle/svc.cpp index 0cf3de75c..cdcdea36d 100644 --- a/src/core/hle/svc.cpp +++ b/src/core/hle/svc.cpp @@ -372,6 +372,10 @@ static void SleepThread(s64 nanoseconds) { // Sleep current thread and check for next thread to schedule Kernel::WaitCurrentThread(WAITTYPE_SLEEP); + + // Create an event to wake the thread up after the specified nanosecond delay has passed + Kernel::WakeThreadAfterDelay(Kernel::GetCurrentThreadHandle(), nanoseconds); + HLE::Reschedule(__func__); } diff --git a/src/core/system.cpp b/src/core/system.cpp index d6188f055..f4c2df1cd 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -21,11 +21,11 @@ void UpdateState(State state) { void Init(EmuWindow* emu_window) { Core::Init(); + CoreTiming::Init(); Memory::Init(); HW::Init(); Kernel::Init(); HLE::Init(); - CoreTiming::Init(); VideoCore::Init(emu_window); } @@ -38,11 +38,11 @@ void RunLoopUntil(u64 global_cycles) { void Shutdown() { VideoCore::Shutdown(); - CoreTiming::Shutdown(); HLE::Shutdown(); Kernel::Shutdown(); HW::Shutdown(); Memory::Shutdown(); + CoreTiming::Shutdown(); Core::Shutdown(); } diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 4df3a5e25..29d220e8d 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -87,8 +87,11 @@ void RendererOpenGL::SwapBuffers() { */ void RendererOpenGL::LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig& framebuffer, const TextureInfo& texture) { + + // TODO: Why are active_fb and the valid framebuffer flipped compared to 3dbrew documentation + // and GSP definitions? const VAddr framebuffer_vaddr = Memory::PhysicalToVirtualAddress( - framebuffer.active_fb == 1 ? framebuffer.address_left2 : framebuffer.address_left1); + framebuffer.active_fb == 0 ? framebuffer.address_left2 : framebuffer.address_left1); LOG_TRACE(Render_OpenGL, "0x%08x bytes from 0x%08x(%dx%d), fmt %x", framebuffer.stride * framebuffer.height, |