early-access version 3136
This commit is contained in:
parent
9e948367b3
commit
54b4c3c044
@ -1,7 +1,7 @@
|
|||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 3133.
|
This is the source code for early-access 3136.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
@ -1,133 +1,41 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/fiber.h"
|
#include "common/fiber.h"
|
||||||
#include "common/spin_lock.h"
|
#include "common/virtual_buffer.h"
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(WIN32)
|
|
||||||
#include <windows.h>
|
|
||||||
#else
|
|
||||||
#include <boost/context/detail/fcontext.hpp>
|
#include <boost/context/detail/fcontext.hpp>
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
constexpr std::size_t default_stack_size = 512 * 1024;
|
constexpr std::size_t default_stack_size = 512 * 1024;
|
||||||
|
|
||||||
struct Fiber::FiberImpl {
|
struct Fiber::FiberImpl {
|
||||||
SpinLock guard{};
|
FiberImpl() : stack{default_stack_size}, rewind_stack{default_stack_size} {}
|
||||||
|
|
||||||
|
VirtualBuffer<u8> stack;
|
||||||
|
VirtualBuffer<u8> rewind_stack;
|
||||||
|
|
||||||
|
std::mutex guard;
|
||||||
std::function<void()> entry_point;
|
std::function<void()> entry_point;
|
||||||
std::function<void()> rewind_point;
|
std::function<void()> rewind_point;
|
||||||
void* rewind_parameter{};
|
|
||||||
std::shared_ptr<Fiber> previous_fiber;
|
std::shared_ptr<Fiber> previous_fiber;
|
||||||
bool is_thread_fiber{};
|
bool is_thread_fiber{};
|
||||||
bool released{};
|
bool released{};
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(WIN32)
|
u8* stack_limit{};
|
||||||
LPVOID handle = nullptr;
|
u8* rewind_stack_limit{};
|
||||||
LPVOID rewind_handle = nullptr;
|
boost::context::detail::fcontext_t context{};
|
||||||
#else
|
boost::context::detail::fcontext_t rewind_context{};
|
||||||
alignas(64) std::array<u8, default_stack_size> stack;
|
|
||||||
alignas(64) std::array<u8, default_stack_size> rewind_stack;
|
|
||||||
u8* stack_limit;
|
|
||||||
u8* rewind_stack_limit;
|
|
||||||
boost::context::detail::fcontext_t context;
|
|
||||||
boost::context::detail::fcontext_t rewind_context;
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void Fiber::SetRewindPoint(std::function<void()>&& rewind_func) {
|
void Fiber::SetRewindPoint(std::function<void()>&& rewind_func) {
|
||||||
impl->rewind_point = std::move(rewind_func);
|
impl->rewind_point = std::move(rewind_func);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(WIN32)
|
|
||||||
|
|
||||||
void Fiber::Start() {
|
|
||||||
ASSERT(impl->previous_fiber != nullptr);
|
|
||||||
impl->previous_fiber->impl->guard.unlock();
|
|
||||||
impl->previous_fiber.reset();
|
|
||||||
impl->entry_point();
|
|
||||||
UNREACHABLE();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::OnRewind() {
|
|
||||||
ASSERT(impl->handle != nullptr);
|
|
||||||
DeleteFiber(impl->handle);
|
|
||||||
impl->handle = impl->rewind_handle;
|
|
||||||
impl->rewind_handle = nullptr;
|
|
||||||
impl->rewind_point();
|
|
||||||
UNREACHABLE();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::FiberStartFunc(void* fiber_parameter) {
|
|
||||||
auto* fiber = static_cast<Fiber*>(fiber_parameter);
|
|
||||||
fiber->Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::RewindStartFunc(void* fiber_parameter) {
|
|
||||||
auto* fiber = static_cast<Fiber*>(fiber_parameter);
|
|
||||||
fiber->OnRewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
Fiber::Fiber(std::function<void()>&& entry_point_func) : impl{std::make_unique<FiberImpl>()} {
|
|
||||||
impl->entry_point = std::move(entry_point_func);
|
|
||||||
impl->handle = CreateFiber(default_stack_size, &FiberStartFunc, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Fiber::Fiber() : impl{std::make_unique<FiberImpl>()} {}
|
|
||||||
|
|
||||||
Fiber::~Fiber() {
|
|
||||||
if (impl->released) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Make sure the Fiber is not being used
|
|
||||||
const bool locked = impl->guard.try_lock();
|
|
||||||
ASSERT_MSG(locked, "Destroying a fiber that's still running");
|
|
||||||
if (locked) {
|
|
||||||
impl->guard.unlock();
|
|
||||||
}
|
|
||||||
DeleteFiber(impl->handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::Exit() {
|
|
||||||
ASSERT_MSG(impl->is_thread_fiber, "Exitting non main thread fiber");
|
|
||||||
if (!impl->is_thread_fiber) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ConvertFiberToThread();
|
|
||||||
impl->guard.unlock();
|
|
||||||
impl->released = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::Rewind() {
|
|
||||||
ASSERT(impl->rewind_point);
|
|
||||||
ASSERT(impl->rewind_handle == nullptr);
|
|
||||||
impl->rewind_handle = CreateFiber(default_stack_size, &RewindStartFunc, this);
|
|
||||||
SwitchToFiber(impl->rewind_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) {
|
|
||||||
ASSERT_MSG(from != nullptr, "Yielding fiber is null!");
|
|
||||||
ASSERT_MSG(to != nullptr, "Next fiber is null!");
|
|
||||||
to->impl->guard.lock();
|
|
||||||
to->impl->previous_fiber = from;
|
|
||||||
SwitchToFiber(to->impl->handle);
|
|
||||||
ASSERT(from->impl->previous_fiber != nullptr);
|
|
||||||
from->impl->previous_fiber->impl->guard.unlock();
|
|
||||||
from->impl->previous_fiber.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Fiber> Fiber::ThreadToFiber() {
|
|
||||||
std::shared_ptr<Fiber> fiber = std::shared_ptr<Fiber>{new Fiber()};
|
|
||||||
fiber->impl->guard.lock();
|
|
||||||
fiber->impl->handle = ConvertThreadToFiber(nullptr);
|
|
||||||
fiber->impl->is_thread_fiber = true;
|
|
||||||
return fiber;
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
void Fiber::Start(boost::context::detail::transfer_t& transfer) {
|
void Fiber::Start(boost::context::detail::transfer_t& transfer) {
|
||||||
ASSERT(impl->previous_fiber != nullptr);
|
ASSERT(impl->previous_fiber != nullptr);
|
||||||
impl->previous_fiber->impl->context = transfer.fctx;
|
impl->previous_fiber->impl->context = transfer.fctx;
|
||||||
@ -199,17 +107,23 @@ void Fiber::Rewind() {
|
|||||||
boost::context::detail::jump_fcontext(impl->rewind_context, this);
|
boost::context::detail::jump_fcontext(impl->rewind_context, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fiber::YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to) {
|
void Fiber::YieldTo(std::weak_ptr<Fiber> weak_from, Fiber& to) {
|
||||||
ASSERT_MSG(from != nullptr, "Yielding fiber is null!");
|
to.impl->guard.lock();
|
||||||
ASSERT_MSG(to != nullptr, "Next fiber is null!");
|
to.impl->previous_fiber = weak_from.lock();
|
||||||
to->impl->guard.lock();
|
|
||||||
to->impl->previous_fiber = from;
|
auto transfer = boost::context::detail::jump_fcontext(to.impl->context, &to);
|
||||||
auto transfer = boost::context::detail::jump_fcontext(to->impl->context, to.get());
|
|
||||||
ASSERT(from->impl->previous_fiber != nullptr);
|
// "from" might no longer be valid if the thread was killed
|
||||||
|
if (auto from = weak_from.lock()) {
|
||||||
|
if (from->impl->previous_fiber == nullptr) {
|
||||||
|
ASSERT_MSG(false, "previous_fiber is nullptr!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
from->impl->previous_fiber->impl->context = transfer.fctx;
|
from->impl->previous_fiber->impl->context = transfer.fctx;
|
||||||
from->impl->previous_fiber->impl->guard.unlock();
|
from->impl->previous_fiber->impl->guard.unlock();
|
||||||
from->impl->previous_fiber.reset();
|
from->impl->previous_fiber.reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<Fiber> Fiber::ThreadToFiber() {
|
std::shared_ptr<Fiber> Fiber::ThreadToFiber() {
|
||||||
std::shared_ptr<Fiber> fiber = std::shared_ptr<Fiber>{new Fiber()};
|
std::shared_ptr<Fiber> fiber = std::shared_ptr<Fiber>{new Fiber()};
|
||||||
@ -218,5 +132,4 @@ std::shared_ptr<Fiber> Fiber::ThreadToFiber() {
|
|||||||
return fiber;
|
return fiber;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
} // namespace Common
|
} // namespace Common
|
@ -6,11 +6,9 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#if !defined(_WIN32) && !defined(WIN32)
|
|
||||||
namespace boost::context::detail {
|
namespace boost::context::detail {
|
||||||
struct transfer_t;
|
struct transfer_t;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
@ -42,7 +40,7 @@ public:
|
|||||||
|
|
||||||
/// Yields control from Fiber 'from' to Fiber 'to'
|
/// Yields control from Fiber 'from' to Fiber 'to'
|
||||||
/// Fiber 'from' must be the currently running fiber.
|
/// Fiber 'from' must be the currently running fiber.
|
||||||
static void YieldTo(std::shared_ptr<Fiber> from, std::shared_ptr<Fiber> to);
|
static void YieldTo(std::weak_ptr<Fiber> weak_from, Fiber& to);
|
||||||
[[nodiscard]] static std::shared_ptr<Fiber> ThreadToFiber();
|
[[nodiscard]] static std::shared_ptr<Fiber> ThreadToFiber();
|
||||||
|
|
||||||
void SetRewindPoint(std::function<void()>&& rewind_func);
|
void SetRewindPoint(std::function<void()>&& rewind_func);
|
||||||
@ -52,23 +50,13 @@ public:
|
|||||||
/// Only call from main thread's fiber
|
/// Only call from main thread's fiber
|
||||||
void Exit();
|
void Exit();
|
||||||
|
|
||||||
/// Changes the start parameter of the fiber. Has no effect if the fiber already started
|
|
||||||
void SetStartParameter(void* new_parameter);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Fiber();
|
Fiber();
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(WIN32)
|
|
||||||
void OnRewind();
|
|
||||||
void Start();
|
|
||||||
static void FiberStartFunc(void* fiber_parameter);
|
|
||||||
static void RewindStartFunc(void* fiber_parameter);
|
|
||||||
#else
|
|
||||||
void OnRewind(boost::context::detail::transfer_t& transfer);
|
void OnRewind(boost::context::detail::transfer_t& transfer);
|
||||||
void Start(boost::context::detail::transfer_t& transfer);
|
void Start(boost::context::detail::transfer_t& transfer);
|
||||||
static void FiberStartFunc(boost::context::detail::transfer_t transfer);
|
static void FiberStartFunc(boost::context::detail::transfer_t transfer);
|
||||||
static void RewindStartFunc(boost::context::detail::transfer_t transfer);
|
static void RewindStartFunc(boost::context::detail::transfer_t transfer);
|
||||||
#endif
|
|
||||||
|
|
||||||
struct FiberImpl;
|
struct FiberImpl;
|
||||||
std::unique_ptr<FiberImpl> impl;
|
std::unique_ptr<FiberImpl> impl;
|
||||||
|
@ -180,7 +180,7 @@ void CpuManager::ShutdownThread() {
|
|||||||
auto* thread = kernel.GetCurrentEmuThread();
|
auto* thread = kernel.GetCurrentEmuThread();
|
||||||
auto core = is_multicore ? kernel.CurrentPhysicalCoreIndex() : 0;
|
auto core = is_multicore ? kernel.CurrentPhysicalCoreIndex() : 0;
|
||||||
|
|
||||||
Common::Fiber::YieldTo(thread->GetHostContext(), core_data[core].host_context);
|
Common::Fiber::YieldTo(thread->GetHostContext(), *core_data[core].host_context);
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ void CpuManager::RunThread(std::size_t core) {
|
|||||||
auto* thread = scheduler.GetSchedulerCurrentThread();
|
auto* thread = scheduler.GetSchedulerCurrentThread();
|
||||||
Kernel::SetCurrentThread(kernel, thread);
|
Kernel::SetCurrentThread(kernel, thread);
|
||||||
|
|
||||||
Common::Fiber::YieldTo(data.host_context, thread->GetHostContext());
|
Common::Fiber::YieldTo(data.host_context, *thread->GetHostContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
@ -116,7 +116,7 @@ void KScheduler::PreemptSingleCore() {
|
|||||||
auto& previous_scheduler = kernel.Scheduler(thread->GetCurrentCore());
|
auto& previous_scheduler = kernel.Scheduler(thread->GetCurrentCore());
|
||||||
previous_scheduler.Unload(thread);
|
previous_scheduler.Unload(thread);
|
||||||
|
|
||||||
Common::Fiber::YieldTo(thread->GetHostContext(), m_switch_fiber);
|
Common::Fiber::YieldTo(thread->GetHostContext(), *m_switch_fiber);
|
||||||
|
|
||||||
GetCurrentThread(kernel).EnableDispatch();
|
GetCurrentThread(kernel).EnableDispatch();
|
||||||
}
|
}
|
||||||
@ -411,7 +411,7 @@ void KScheduler::ScheduleImpl() {
|
|||||||
m_switch_cur_thread = cur_thread;
|
m_switch_cur_thread = cur_thread;
|
||||||
m_switch_highest_priority_thread = highest_priority_thread;
|
m_switch_highest_priority_thread = highest_priority_thread;
|
||||||
m_switch_from_schedule = true;
|
m_switch_from_schedule = true;
|
||||||
Common::Fiber::YieldTo(cur_thread->host_context, m_switch_fiber);
|
Common::Fiber::YieldTo(cur_thread->host_context, *m_switch_fiber);
|
||||||
|
|
||||||
// Returning from ScheduleImpl occurs after this thread has been scheduled again.
|
// Returning from ScheduleImpl occurs after this thread has been scheduled again.
|
||||||
}
|
}
|
||||||
@ -489,7 +489,7 @@ void KScheduler::ScheduleImplFiber() {
|
|||||||
Reload(highest_priority_thread);
|
Reload(highest_priority_thread);
|
||||||
|
|
||||||
// Reload the host thread.
|
// Reload the host thread.
|
||||||
Common::Fiber::YieldTo(m_switch_fiber, highest_priority_thread->host_context);
|
Common::Fiber::YieldTo(m_switch_fiber, *highest_priority_thread->host_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KScheduler::Unload(KThread* thread) {
|
void KScheduler::Unload(KThread* thread) {
|
||||||
|
@ -50,7 +50,7 @@ public:
|
|||||||
value++;
|
value++;
|
||||||
}
|
}
|
||||||
results[id] = value;
|
results[id] = value;
|
||||||
Fiber::YieldTo(work_fibers[id], thread_fibers[id]);
|
Fiber::YieldTo(work_fibers[id], *thread_fibers[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExecuteThread(u32 id);
|
void ExecuteThread(u32 id);
|
||||||
@ -68,7 +68,7 @@ void TestControl1::ExecuteThread(u32 id) {
|
|||||||
thread_fibers[id] = thread_fiber;
|
thread_fibers[id] = thread_fiber;
|
||||||
work_fibers[id] = std::make_shared<Fiber>([this] { DoWork(); });
|
work_fibers[id] = std::make_shared<Fiber>([this] { DoWork(); });
|
||||||
items[id] = rand() % 256;
|
items[id] = rand() % 256;
|
||||||
Fiber::YieldTo(thread_fibers[id], work_fibers[id]);
|
Fiber::YieldTo(thread_fibers[id], *work_fibers[id]);
|
||||||
thread_fibers[id]->Exit();
|
thread_fibers[id]->Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,11 +105,11 @@ public:
|
|||||||
for (u32 i = 0; i < 12000; i++) {
|
for (u32 i = 0; i < 12000; i++) {
|
||||||
value1 += i;
|
value1 += i;
|
||||||
}
|
}
|
||||||
Fiber::YieldTo(fiber1, fiber3);
|
Fiber::YieldTo(fiber1, *fiber3);
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
assert1 = id == 1;
|
assert1 = id == 1;
|
||||||
value2 += 5000;
|
value2 += 5000;
|
||||||
Fiber::YieldTo(fiber1, thread_fibers[id]);
|
Fiber::YieldTo(fiber1, *thread_fibers[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DoWork2() {
|
void DoWork2() {
|
||||||
@ -117,7 +117,7 @@ public:
|
|||||||
;
|
;
|
||||||
value2 = 2000;
|
value2 = 2000;
|
||||||
trap = false;
|
trap = false;
|
||||||
Fiber::YieldTo(fiber2, fiber1);
|
Fiber::YieldTo(fiber2, *fiber1);
|
||||||
assert3 = false;
|
assert3 = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,19 +125,19 @@ public:
|
|||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
assert2 = id == 0;
|
assert2 = id == 0;
|
||||||
value1 += 1000;
|
value1 += 1000;
|
||||||
Fiber::YieldTo(fiber3, thread_fibers[id]);
|
Fiber::YieldTo(fiber3, *thread_fibers[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExecuteThread(u32 id);
|
void ExecuteThread(u32 id);
|
||||||
|
|
||||||
void CallFiber1() {
|
void CallFiber1() {
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
Fiber::YieldTo(thread_fibers[id], fiber1);
|
Fiber::YieldTo(thread_fibers[id], *fiber1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CallFiber2() {
|
void CallFiber2() {
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
Fiber::YieldTo(thread_fibers[id], fiber2);
|
Fiber::YieldTo(thread_fibers[id], *fiber2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Exit();
|
void Exit();
|
||||||
@ -207,23 +207,23 @@ public:
|
|||||||
|
|
||||||
void DoWork1() {
|
void DoWork1() {
|
||||||
value1 += 1;
|
value1 += 1;
|
||||||
Fiber::YieldTo(fiber1, fiber2);
|
Fiber::YieldTo(fiber1, *fiber2);
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
value3 += 1;
|
value3 += 1;
|
||||||
Fiber::YieldTo(fiber1, thread_fibers[id]);
|
Fiber::YieldTo(fiber1, *thread_fibers[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DoWork2() {
|
void DoWork2() {
|
||||||
value2 += 1;
|
value2 += 1;
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
Fiber::YieldTo(fiber2, thread_fibers[id]);
|
Fiber::YieldTo(fiber2, *thread_fibers[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExecuteThread(u32 id);
|
void ExecuteThread(u32 id);
|
||||||
|
|
||||||
void CallFiber1() {
|
void CallFiber1() {
|
||||||
const u32 id = thread_ids.Get();
|
const u32 id = thread_ids.Get();
|
||||||
Fiber::YieldTo(thread_fibers[id], fiber1);
|
Fiber::YieldTo(thread_fibers[id], *fiber1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Exit();
|
void Exit();
|
||||||
@ -283,7 +283,7 @@ public:
|
|||||||
|
|
||||||
void Execute() {
|
void Execute() {
|
||||||
thread_fiber = Fiber::ThreadToFiber();
|
thread_fiber = Fiber::ThreadToFiber();
|
||||||
Fiber::YieldTo(thread_fiber, fiber1);
|
Fiber::YieldTo(thread_fiber, *fiber1);
|
||||||
thread_fiber->Exit();
|
thread_fiber->Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ public:
|
|||||||
fiber1->SetRewindPoint([this] { DoWork(); });
|
fiber1->SetRewindPoint([this] { DoWork(); });
|
||||||
if (rewinded) {
|
if (rewinded) {
|
||||||
goal_reached = true;
|
goal_reached = true;
|
||||||
Fiber::YieldTo(fiber1, thread_fiber);
|
Fiber::YieldTo(fiber1, *thread_fiber);
|
||||||
}
|
}
|
||||||
rewinded = true;
|
rewinded = true;
|
||||||
fiber1->Rewind();
|
fiber1->Rewind();
|
||||||
|
Loading…
Reference in New Issue
Block a user