early-access version 2003
This commit is contained in:
parent
dde6e66bd1
commit
07a248f589
5 changed files with 166 additions and 216 deletions
|
@ -1,7 +1,7 @@
|
||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 2000.
|
This is the source code for early-access 2003.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
|
|
@ -4,242 +4,175 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
// a simple lockless thread-safe,
|
||||||
|
// single reader, single writer queue
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <iostream>
|
#include <cstddef>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <utility>
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
/// a more foolproof multiple reader, multiple writer queue
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
class MPMCQueue {
|
class SPSCQueue {
|
||||||
#define ABORT() \
|
|
||||||
do { \
|
|
||||||
std::cerr << __FILE__ " ERR " << __LINE__ << std::endl; \
|
|
||||||
abort(); \
|
|
||||||
} while (0)
|
|
||||||
public:
|
public:
|
||||||
~MPMCQueue() {
|
SPSCQueue() {
|
||||||
Clear();
|
write_ptr = read_ptr = new ElementPtr();
|
||||||
if (waiting || head || tail) {
|
|
||||||
// Remove all the ABORT() after 1 month merged without problems
|
|
||||||
ABORT();
|
|
||||||
}
|
}
|
||||||
|
~SPSCQueue() {
|
||||||
|
// this will empty out the whole queue
|
||||||
|
delete read_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t Size() const {
|
||||||
|
return size.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Empty() const {
|
||||||
|
return Size() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] T& Front() const {
|
||||||
|
return read_ptr->current;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Arg>
|
template <typename Arg>
|
||||||
void Push(Arg&& t) {
|
void Push(Arg&& t) {
|
||||||
Node* const node = new Node(std::forward<Arg>(t));
|
// create the element, add it to the queue
|
||||||
if (!node || node == PLACEHOLDER) {
|
write_ptr->current = std::forward<Arg>(t);
|
||||||
ABORT();
|
// set the next pointer to a new element ptr
|
||||||
}
|
// then advance the write pointer
|
||||||
while (true) {
|
ElementPtr* new_ptr = new ElementPtr();
|
||||||
if (Node* const previous = tail.load(ACQUIRE)) {
|
write_ptr->next.store(new_ptr, std::memory_order_release);
|
||||||
if (Node* exchange = nullptr;
|
write_ptr = new_ptr;
|
||||||
!previous->next.compare_exchange_weak(exchange, node, ACQ_REL)) {
|
++size;
|
||||||
continue;
|
|
||||||
}
|
// cv_mutex must be held or else there will be a missed wakeup if the other thread is in the
|
||||||
if (tail.exchange(node, ACQ_REL) != previous) {
|
// line before cv.wait
|
||||||
ABORT();
|
// TODO(bunnei): This can be replaced with C++20 waitable atomics when properly supported.
|
||||||
}
|
// See discussion on https://github.com/yuzu-emu/yuzu/pull/3173 for details.
|
||||||
} else {
|
std::lock_guard lock{cv_mutex};
|
||||||
if (Node* exchange = nullptr;
|
cv.notify_one();
|
||||||
!tail.compare_exchange_weak(exchange, node, ACQ_REL)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Node* exchange = nullptr;
|
|
||||||
!head.compare_exchange_weak(exchange, node, ACQ_REL);)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (waiting.load(ACQUIRE)) {
|
|
||||||
std::lock_guard lock{mutex};
|
|
||||||
condition.notify_one();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Pop() {
|
||||||
|
--size;
|
||||||
|
|
||||||
|
ElementPtr* tmpptr = read_ptr;
|
||||||
|
// advance the read pointer
|
||||||
|
read_ptr = tmpptr->next.load();
|
||||||
|
// set the next element to nullptr to stop the recursive deletion
|
||||||
|
tmpptr->next.store(nullptr);
|
||||||
|
delete tmpptr; // this also deletes the element
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pop(T& t) {
|
bool Pop(T& t) {
|
||||||
return PopImpl<false>(t);
|
if (Empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
--size;
|
||||||
|
|
||||||
|
ElementPtr* tmpptr = read_ptr;
|
||||||
|
read_ptr = tmpptr->next.load(std::memory_order_acquire);
|
||||||
|
t = std::move(tmpptr->current);
|
||||||
|
tmpptr->next.store(nullptr);
|
||||||
|
delete tmpptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wait() {
|
||||||
|
if (Empty()) {
|
||||||
|
std::unique_lock lock{cv_mutex};
|
||||||
|
cv.wait(lock, [this]() { return !Empty(); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
T PopWait() {
|
T PopWait() {
|
||||||
|
Wait();
|
||||||
T t;
|
T t;
|
||||||
if (!PopImpl<true>(t)) {
|
Pop(t);
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Wait() {
|
// not thread-safe
|
||||||
if (head.load(ACQUIRE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
static_cast<void>(waiting.fetch_add(1, ACQ_REL));
|
|
||||||
std::unique_lock lock{mutex};
|
|
||||||
while (true) {
|
|
||||||
if (head.load(ACQUIRE)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
condition.wait(lock);
|
|
||||||
}
|
|
||||||
if (!waiting.fetch_sub(1, ACQ_REL)) {
|
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Clear() {
|
void Clear() {
|
||||||
while (true) {
|
size.store(0);
|
||||||
Node* const last = tail.load(ACQUIRE);
|
delete read_ptr;
|
||||||
if (!last) {
|
write_ptr = read_ptr = new ElementPtr();
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Node* exchange = nullptr;
|
|
||||||
!last->next.compare_exchange_weak(exchange, PLACEHOLDER, ACQ_REL)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tail.exchange(nullptr, ACQ_REL) != last) {
|
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
Node* node = head.exchange(nullptr, ACQ_REL);
|
|
||||||
while (node && node != PLACEHOLDER) {
|
|
||||||
Node* next = node->next.load(ACQUIRE);
|
|
||||||
delete node;
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
template <bool WAIT>
|
// stores a pointer to element
|
||||||
bool PopImpl(T& t) {
|
// and a pointer to the next ElementPtr
|
||||||
std::optional<std::unique_lock<std::mutex>> lock{std::nullopt};
|
class ElementPtr {
|
||||||
while (true) {
|
public:
|
||||||
Node* const node = head.load(ACQUIRE);
|
ElementPtr() {}
|
||||||
if (!node) {
|
~ElementPtr() {
|
||||||
if constexpr (!WAIT) {
|
ElementPtr* next_ptr = next.load();
|
||||||
return false;
|
|
||||||
}
|
if (next_ptr)
|
||||||
if (!lock) {
|
delete next_ptr;
|
||||||
static_cast<void>(waiting.fetch_add(1, ACQ_REL));
|
|
||||||
lock = std::unique_lock{mutex};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
condition.wait(*lock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Node* const next = node->next.load(ACQUIRE);
|
|
||||||
if (next) {
|
|
||||||
if (next == PLACEHOLDER) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Node* exchange = node; !head.compare_exchange_weak(exchange, next, ACQ_REL)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (Node* exchange = nullptr;
|
|
||||||
!node->next.compare_exchange_weak(exchange, PLACEHOLDER, ACQ_REL)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tail.exchange(nullptr, ACQ_REL) != node) {
|
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
if (head.exchange(nullptr, ACQ_REL) != node) {
|
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t = std::move(node->value);
|
|
||||||
delete node;
|
|
||||||
if (lock) {
|
|
||||||
if (!waiting.fetch_sub(1, ACQ_REL)) {
|
|
||||||
ABORT();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Node {
|
T current;
|
||||||
template <typename Arg>
|
std::atomic<ElementPtr*> next{nullptr};
|
||||||
explicit Node(Arg&& t) : value{std::forward<Arg>(t)} {}
|
|
||||||
|
|
||||||
Node(const Node&) = delete;
|
|
||||||
Node& operator=(const Node&) = delete;
|
|
||||||
|
|
||||||
Node(Node&&) = delete;
|
|
||||||
Node& operator=(Node&&) = delete;
|
|
||||||
|
|
||||||
const T value;
|
|
||||||
std::atomic<Node*> next{nullptr};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// We only need to avoid SEQ_CST on X86
|
ElementPtr* write_ptr;
|
||||||
// We can add RELAXED later if we port to ARM and it's too slow
|
ElementPtr* read_ptr;
|
||||||
static constexpr auto ACQUIRE = std::memory_order_acquire;
|
std::atomic_size_t size{0};
|
||||||
static constexpr auto ACQ_REL = std::memory_order_acq_rel;
|
std::mutex cv_mutex;
|
||||||
static inline const auto PLACEHOLDER = reinterpret_cast<Node*>(1);
|
std::condition_variable cv;
|
||||||
|
|
||||||
std::atomic<Node*> head{nullptr};
|
|
||||||
std::atomic<Node*> tail{nullptr};
|
|
||||||
|
|
||||||
std::atomic_size_t waiting{0};
|
|
||||||
std::condition_variable condition{};
|
|
||||||
std::mutex mutex{};
|
|
||||||
#undef ABORT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// a simple lockless thread-safe,
|
// a simple thread-safe,
|
||||||
/// single reader, single writer queue
|
// single reader, multiple writer queue
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
class /*[[deprecated("Transition to MPMCQueue")]]*/ SPSCQueue {
|
class MPSCQueue {
|
||||||
public:
|
public:
|
||||||
|
[[nodiscard]] std::size_t Size() const {
|
||||||
|
return spsc_queue.Size();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Empty() const {
|
||||||
|
return spsc_queue.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] T& Front() const {
|
||||||
|
return spsc_queue.Front();
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Arg>
|
template <typename Arg>
|
||||||
void Push(Arg&& t) {
|
void Push(Arg&& t) {
|
||||||
queue.Push(std::forward<Arg>(t));
|
std::lock_guard lock{write_lock};
|
||||||
|
spsc_queue.Push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pop() {
|
||||||
|
return spsc_queue.Pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pop(T& t) {
|
bool Pop(T& t) {
|
||||||
return queue.Pop(t);
|
return spsc_queue.Pop(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Wait() {
|
void Wait() {
|
||||||
queue.Wait();
|
spsc_queue.Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
T PopWait() {
|
T PopWait() {
|
||||||
return queue.PopWait();
|
return spsc_queue.PopWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not thread-safe
|
||||||
void Clear() {
|
void Clear() {
|
||||||
queue.Clear();
|
spsc_queue.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MPMCQueue<T> queue{};
|
SPSCQueue<T> spsc_queue;
|
||||||
};
|
std::mutex write_lock;
|
||||||
|
|
||||||
/// a simple thread-safe,
|
|
||||||
/// single reader, multiple writer queue
|
|
||||||
template <typename T>
|
|
||||||
class /*[[deprecated("Transition to MPMCQueue")]]*/ MPSCQueue {
|
|
||||||
public:
|
|
||||||
template <typename Arg>
|
|
||||||
void Push(Arg&& t) {
|
|
||||||
queue.Push(std::forward<Arg>(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Pop(T& t) {
|
|
||||||
return queue.Pop(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
T PopWait() {
|
|
||||||
return queue.PopWait();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
MPMCQueue<T> queue{};
|
|
||||||
};
|
};
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
|
@ -16,6 +16,30 @@
|
||||||
|
|
||||||
namespace Service::AM::Applets {
|
namespace Service::AM::Applets {
|
||||||
|
|
||||||
|
struct ErrorCode {
|
||||||
|
u32 error_category{};
|
||||||
|
u32 error_number{};
|
||||||
|
|
||||||
|
static constexpr ErrorCode FromU64(u64 error_code) {
|
||||||
|
return {
|
||||||
|
.error_category{static_cast<u32>(error_code >> 32)},
|
||||||
|
.error_number{static_cast<u32>(error_code & 0xFFFFFFFF)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr ErrorCode FromResultCode(ResultCode result) {
|
||||||
|
return {
|
||||||
|
.error_category{2000 + static_cast<u32>(result.module.Value())},
|
||||||
|
.error_number{result.description.Value()},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ResultCode ToResultCode() const {
|
||||||
|
return ResultCode{static_cast<ErrorModule>(error_category - 2000), error_number};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(ErrorCode) == 0x8, "ErrorCode has incorrect size.");
|
||||||
|
|
||||||
#pragma pack(push, 4)
|
#pragma pack(push, 4)
|
||||||
struct ShowError {
|
struct ShowError {
|
||||||
u8 mode;
|
u8 mode;
|
||||||
|
@ -76,12 +100,7 @@ void CopyArgumentData(const std::vector<u8>& data, T& variable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode Decode64BitError(u64 error) {
|
ResultCode Decode64BitError(u64 error) {
|
||||||
const auto description = (error >> 32) & 0x1FFF;
|
return ErrorCode::FromU64(error).ToResultCode();
|
||||||
auto module = error & 0x3FF;
|
|
||||||
if (module >= 2000)
|
|
||||||
module -= 2000;
|
|
||||||
module &= 0x1FF;
|
|
||||||
return {static_cast<ErrorModule>(module), static_cast<u32>(description)};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
|
@ -298,14 +298,10 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, Id vertex) {
|
||||||
if (IR::IsGeneric(attr)) {
|
if (IR::IsGeneric(attr)) {
|
||||||
const u32 index{IR::GenericAttributeIndex(attr)};
|
const u32 index{IR::GenericAttributeIndex(attr)};
|
||||||
const std::optional<AttrInfo> type{AttrTypes(ctx, index)};
|
const std::optional<AttrInfo> type{AttrTypes(ctx, index)};
|
||||||
if (!type) {
|
if (!type || !ctx.runtime_info.previous_stage_stores.Generic(index, element)) {
|
||||||
// Attribute is disabled
|
// Attribute is disabled or varying component is not written
|
||||||
return ctx.Const(element == 3 ? 1.0f : 0.0f);
|
return ctx.Const(element == 3 ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
if (!ctx.runtime_info.previous_stage_stores.Generic(index, element)) {
|
|
||||||
// Varying component is not written
|
|
||||||
return ctx.Const(type && element == 3 ? 1.0f : 0.0f);
|
|
||||||
}
|
|
||||||
const Id generic_id{ctx.input_generics.at(index)};
|
const Id generic_id{ctx.input_generics.at(index)};
|
||||||
const Id pointer{AttrPointer(ctx, type->pointer, vertex, generic_id, ctx.Const(element))};
|
const Id pointer{AttrPointer(ctx, type->pointer, vertex, generic_id, ctx.Const(element))};
|
||||||
const Id value{ctx.OpLoad(type->id, pointer)};
|
const Id value{ctx.OpLoad(type->id, pointer)};
|
||||||
|
|
|
@ -664,18 +664,18 @@ void PlayerControlPreview::DrawHandheldController(QPainter& p, const QPointF cen
|
||||||
// Face buttons
|
// Face buttons
|
||||||
p.setPen(colors.outline);
|
p.setPen(colors.outline);
|
||||||
button_color = colors.button;
|
button_color = colors.button;
|
||||||
DrawCircleButton(p, face_center + QPoint(face_distance, 0), button_values[A], face_radius);
|
DrawCircleButton(p, face_center + QPointF(face_distance, 0), button_values[A], face_radius);
|
||||||
DrawCircleButton(p, face_center + QPoint(0, face_distance), button_values[B], face_radius);
|
DrawCircleButton(p, face_center + QPointF(0, face_distance), button_values[B], face_radius);
|
||||||
DrawCircleButton(p, face_center + QPoint(0, -face_distance), button_values[X], face_radius);
|
DrawCircleButton(p, face_center + QPointF(0, -face_distance), button_values[X], face_radius);
|
||||||
DrawCircleButton(p, face_center + QPoint(-face_distance, 0), button_values[Y], face_radius);
|
DrawCircleButton(p, face_center + QPointF(-face_distance, 0), button_values[Y], face_radius);
|
||||||
|
|
||||||
// Face buttons text
|
// Face buttons text
|
||||||
p.setPen(colors.transparent);
|
p.setPen(colors.transparent);
|
||||||
p.setBrush(colors.font);
|
p.setBrush(colors.font);
|
||||||
DrawSymbol(p, face_center + QPoint(face_distance, 0), Symbol::A, text_size);
|
DrawSymbol(p, face_center + QPointF(face_distance, 0), Symbol::A, text_size);
|
||||||
DrawSymbol(p, face_center + QPoint(0, face_distance), Symbol::B, text_size);
|
DrawSymbol(p, face_center + QPointF(0, face_distance), Symbol::B, text_size);
|
||||||
DrawSymbol(p, face_center + QPoint(0, -face_distance), Symbol::X, text_size);
|
DrawSymbol(p, face_center + QPointF(0, -face_distance), Symbol::X, text_size);
|
||||||
DrawSymbol(p, face_center + QPoint(-face_distance, 1), Symbol::Y, text_size);
|
DrawSymbol(p, face_center + QPointF(-face_distance, 1), Symbol::Y, text_size);
|
||||||
|
|
||||||
// D-pad constants
|
// D-pad constants
|
||||||
const QPointF dpad_center = center + QPoint(-171, 8);
|
const QPointF dpad_center = center + QPoint(-171, 8);
|
||||||
|
@ -686,18 +686,20 @@ void PlayerControlPreview::DrawHandheldController(QPainter& p, const QPointF cen
|
||||||
// D-pad buttons
|
// D-pad buttons
|
||||||
p.setPen(colors.outline);
|
p.setPen(colors.outline);
|
||||||
button_color = colors.button;
|
button_color = colors.button;
|
||||||
DrawCircleButton(p, dpad_center + QPoint(dpad_distance, 0), button_values[DRight], dpad_radius);
|
DrawCircleButton(p, dpad_center + QPointF(dpad_distance, 0), button_values[DRight],
|
||||||
DrawCircleButton(p, dpad_center + QPoint(0, dpad_distance), button_values[DDown], dpad_radius);
|
dpad_radius);
|
||||||
DrawCircleButton(p, dpad_center + QPoint(0, -dpad_distance), button_values[DUp], dpad_radius);
|
DrawCircleButton(p, dpad_center + QPointF(0, dpad_distance), button_values[DDown], dpad_radius);
|
||||||
DrawCircleButton(p, dpad_center + QPoint(-dpad_distance, 0), button_values[DLeft], dpad_radius);
|
DrawCircleButton(p, dpad_center + QPointF(0, -dpad_distance), button_values[DUp], dpad_radius);
|
||||||
|
DrawCircleButton(p, dpad_center + QPointF(-dpad_distance, 0), button_values[DLeft],
|
||||||
|
dpad_radius);
|
||||||
|
|
||||||
// D-pad arrows
|
// D-pad arrows
|
||||||
p.setPen(colors.font2);
|
p.setPen(colors.font2);
|
||||||
p.setBrush(colors.font2);
|
p.setBrush(colors.font2);
|
||||||
DrawArrow(p, dpad_center + QPoint(dpad_distance, 0), Direction::Right, dpad_arrow_size);
|
DrawArrow(p, dpad_center + QPointF(dpad_distance, 0), Direction::Right, dpad_arrow_size);
|
||||||
DrawArrow(p, dpad_center + QPoint(0, dpad_distance), Direction::Down, dpad_arrow_size);
|
DrawArrow(p, dpad_center + QPointF(0, dpad_distance), Direction::Down, dpad_arrow_size);
|
||||||
DrawArrow(p, dpad_center + QPoint(0, -dpad_distance), Direction::Up, dpad_arrow_size);
|
DrawArrow(p, dpad_center + QPointF(0, -dpad_distance), Direction::Up, dpad_arrow_size);
|
||||||
DrawArrow(p, dpad_center + QPoint(-dpad_distance, 0), Direction::Left, dpad_arrow_size);
|
DrawArrow(p, dpad_center + QPointF(-dpad_distance, 0), Direction::Left, dpad_arrow_size);
|
||||||
|
|
||||||
// ZL and ZR buttons
|
// ZL and ZR buttons
|
||||||
p.setPen(colors.outline);
|
p.setPen(colors.outline);
|
||||||
|
|
Loading…
Reference in a new issue