mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-05 03:54:32 +00:00
Compare commits
3 Commits
fx-shaders
...
fast-fix
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f5ef90bca | |||
| c10f473f0d | |||
| dad3ce6a33 |
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/service/am/display_layer_manager.h"
|
#include "core/hle/service/am/display_layer_manager.h"
|
||||||
#include "core/hle/service/nvnflinger/hwc_layer.h"
|
|
||||||
#include "core/hle/service/sm/sm.h"
|
#include "core/hle/service/sm/sm.h"
|
||||||
#include "core/hle/service/vi/application_display_service.h"
|
#include "core/hle/service/vi/application_display_service.h"
|
||||||
#include "core/hle/service/vi/container.h"
|
#include "core/hle/service/vi/container.h"
|
||||||
@@ -34,7 +33,6 @@ void DisplayLayerManager::Initialize(Core::System& system, Kernel::KProcess* pro
|
|||||||
m_system_shared_buffer_id = 0;
|
m_system_shared_buffer_id = 0;
|
||||||
m_system_shared_layer_id = 0;
|
m_system_shared_layer_id = 0;
|
||||||
m_applet_id = applet_id;
|
m_applet_id = applet_id;
|
||||||
m_library_applet_mode = mode;
|
|
||||||
m_buffer_sharing_enabled = false;
|
m_buffer_sharing_enabled = false;
|
||||||
m_blending_enabled = mode == LibraryAppletMode::PartialForeground ||
|
m_blending_enabled = mode == LibraryAppletMode::PartialForeground ||
|
||||||
mode == LibraryAppletMode::PartialForegroundIndirectDisplay;
|
mode == LibraryAppletMode::PartialForegroundIndirectDisplay;
|
||||||
@@ -74,16 +72,14 @@ Result DisplayLayerManager::CreateManagedDisplayLayer(u64* out_layer_id) {
|
|||||||
out_layer_id, 0, display_id, Service::AppletResourceUserId{m_process->GetProcessId()}));
|
out_layer_id, 0, display_id, Service::AppletResourceUserId{m_process->GetProcessId()}));
|
||||||
|
|
||||||
m_manager_display_service->SetLayerVisibility(m_visible, *out_layer_id);
|
m_manager_display_service->SetLayerVisibility(m_visible, *out_layer_id);
|
||||||
(void)m_display_service->GetContainer()->SetLayerStackMask(*out_layer_id,
|
|
||||||
this->GetLayerStackMask());
|
|
||||||
|
|
||||||
if (m_applet_id != AppletId::Application) {
|
if (m_applet_id != AppletId::Application) {
|
||||||
(void)m_manager_display_service->SetLayerBlending(m_blending_enabled, *out_layer_id);
|
(void)m_manager_display_service->SetLayerBlending(m_blending_enabled, *out_layer_id);
|
||||||
if (m_applet_id == AppletId::OverlayDisplay) {
|
if (m_applet_id == AppletId::OverlayDisplay) {
|
||||||
(void)m_manager_display_service->SetLayerZIndex(Overlay, *out_layer_id);
|
(void)m_manager_display_service->SetLayerZIndex(-1, *out_layer_id);
|
||||||
(void)m_display_service->GetContainer()->SetLayerIsOverlay(*out_layer_id, true);
|
(void)m_display_service->GetContainer()->SetLayerIsOverlay(*out_layer_id, true);
|
||||||
} else {
|
} else {
|
||||||
(void)m_manager_display_service->SetLayerZIndex(Foreground, *out_layer_id);
|
(void)m_manager_display_service->SetLayerZIndex(1, *out_layer_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +122,10 @@ Result DisplayLayerManager::IsSystemBufferSharingEnabled() {
|
|||||||
|
|
||||||
// Ensure the overlay layer is visible
|
// Ensure the overlay layer is visible
|
||||||
m_manager_display_service->SetLayerVisibility(m_visible, m_system_shared_layer_id);
|
m_manager_display_service->SetLayerVisibility(m_visible, m_system_shared_layer_id);
|
||||||
(void)m_display_service->GetContainer()->SetLayerStackMask(m_system_shared_layer_id,
|
|
||||||
this->GetLayerStackMask());
|
|
||||||
m_manager_display_service->SetLayerBlending(m_blending_enabled, m_system_shared_layer_id);
|
m_manager_display_service->SetLayerBlending(m_blending_enabled, m_system_shared_layer_id);
|
||||||
s32 initial_z = Foreground;
|
s32 initial_z = 1;
|
||||||
if (m_applet_id == AppletId::OverlayDisplay) {
|
if (m_applet_id == AppletId::OverlayDisplay) {
|
||||||
initial_z = Overlay;
|
initial_z = -1;
|
||||||
(void)m_display_service->GetContainer()->SetLayerIsOverlay(m_system_shared_layer_id, true);
|
(void)m_display_service->GetContainer()->SetLayerIsOverlay(m_system_shared_layer_id, true);
|
||||||
}
|
}
|
||||||
m_manager_display_service->SetLayerZIndex(initial_z, m_system_shared_layer_id);
|
m_manager_display_service->SetLayerZIndex(initial_z, m_system_shared_layer_id);
|
||||||
@@ -148,36 +142,6 @@ Result DisplayLayerManager::GetSystemSharedLayerHandle(u64* out_system_shared_bu
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 DisplayLayerManager::GetLayerStackMask() const {
|
|
||||||
using Nvnflinger::LayerStackBit;
|
|
||||||
using Nvnflinger::LayerStackId;
|
|
||||||
|
|
||||||
constexpr u32 Displayed = LayerStackBit(LayerStackId::Default);
|
|
||||||
constexpr u32 Screenshot = LayerStackBit(LayerStackId::Screenshot);
|
|
||||||
constexpr u32 Recording = LayerStackBit(LayerStackId::Recording);
|
|
||||||
constexpr u32 LastFrame = LayerStackBit(LayerStackId::LastFrame);
|
|
||||||
constexpr u32 Debug = LayerStackBit(LayerStackId::ApplicationForDebug);
|
|
||||||
|
|
||||||
switch (m_applet_id) {
|
|
||||||
case AppletId::Application:
|
|
||||||
return Displayed | Screenshot | Recording | LastFrame | Debug;
|
|
||||||
case AppletId::OverlayDisplay:
|
|
||||||
return Displayed;
|
|
||||||
case AppletId::QLaunch:
|
|
||||||
return Displayed;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (m_library_applet_mode) {
|
|
||||||
case LibraryAppletMode::AllForeground:
|
|
||||||
case LibraryAppletMode::AllForegroundInitiallyHidden:
|
|
||||||
return Displayed | Screenshot | LastFrame;
|
|
||||||
default:
|
|
||||||
return Displayed | Screenshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayLayerManager::SetWindowVisibility(bool visible) {
|
void DisplayLayerManager::SetWindowVisibility(bool visible) {
|
||||||
if (m_visible == visible) {
|
if (m_visible == visible) {
|
||||||
return;
|
return;
|
||||||
@@ -221,17 +185,10 @@ void DisplayLayerManager::SetOverlayZIndex(s32 z_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result DisplayLayerManager::WriteAppletCaptureBuffer(bool* out_was_written,
|
Result DisplayLayerManager::WriteAppletCaptureBuffer(bool* out_was_written,
|
||||||
s32* out_fbshare_layer_index,
|
s32* out_fbshare_layer_index) {
|
||||||
VI::CaptureKind kind) {
|
|
||||||
R_UNLESS(m_buffer_sharing_enabled, VI::ResultPermissionDenied);
|
R_UNLESS(m_buffer_sharing_enabled, VI::ResultPermissionDenied);
|
||||||
R_RETURN(m_display_service->GetContainer()->GetSharedBufferManager()->WriteAppletCaptureBuffer(
|
R_RETURN(m_display_service->GetContainer()->GetSharedBufferManager()->WriteAppletCaptureBuffer(
|
||||||
out_was_written, out_fbshare_layer_index, kind));
|
out_was_written, out_fbshare_layer_index));
|
||||||
}
|
|
||||||
|
|
||||||
Result DisplayLayerManager::ClearAppletCaptureBuffer(s32 fbshare_layer_index, u32 color) {
|
|
||||||
R_UNLESS(m_buffer_sharing_enabled, VI::ResultPermissionDenied);
|
|
||||||
R_RETURN(m_display_service->GetContainer()->GetSharedBufferManager()->ClearAppletCaptureBuffer(
|
|
||||||
fbshare_layer_index, color));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Service::AM
|
} // namespace Service::AM
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
@@ -23,7 +23,6 @@ class KProcess;
|
|||||||
namespace Service::VI {
|
namespace Service::VI {
|
||||||
class IApplicationDisplayService;
|
class IApplicationDisplayService;
|
||||||
class IManagerDisplayService;
|
class IManagerDisplayService;
|
||||||
enum class CaptureKind : u32;
|
|
||||||
} // namespace Service::VI
|
} // namespace Service::VI
|
||||||
|
|
||||||
namespace Service::AM {
|
namespace Service::AM {
|
||||||
@@ -49,13 +48,9 @@ public:
|
|||||||
|
|
||||||
void SetOverlayZIndex(s32 z_index);
|
void SetOverlayZIndex(s32 z_index);
|
||||||
|
|
||||||
Result WriteAppletCaptureBuffer(bool* out_was_written, s32* out_fbshare_layer_index,
|
Result WriteAppletCaptureBuffer(bool* out_was_written, s32* out_fbshare_layer_index);
|
||||||
VI::CaptureKind kind);
|
|
||||||
Result ClearAppletCaptureBuffer(s32 fbshare_layer_index, u32 color);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
u32 GetLayerStackMask() const;
|
|
||||||
|
|
||||||
Kernel::KProcess* m_process{};
|
Kernel::KProcess* m_process{};
|
||||||
std::shared_ptr<VI::IApplicationDisplayService> m_display_service{};
|
std::shared_ptr<VI::IApplicationDisplayService> m_display_service{};
|
||||||
std::shared_ptr<VI::IManagerDisplayService> m_manager_display_service{};
|
std::shared_ptr<VI::IManagerDisplayService> m_manager_display_service{};
|
||||||
@@ -64,7 +59,6 @@ private:
|
|||||||
u64 m_system_shared_buffer_id{};
|
u64 m_system_shared_buffer_id{};
|
||||||
u64 m_system_shared_layer_id{};
|
u64 m_system_shared_layer_id{};
|
||||||
AppletId m_applet_id{};
|
AppletId m_applet_id{};
|
||||||
LibraryAppletMode m_library_applet_mode{};
|
|
||||||
bool m_buffer_sharing_enabled{};
|
bool m_buffer_sharing_enabled{};
|
||||||
bool m_blending_enabled{};
|
bool m_blending_enabled{};
|
||||||
bool m_visible{true};
|
bool m_visible{true};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
#include "core/hle/service/am/applet.h"
|
#include "core/hle/service/am/applet.h"
|
||||||
#include "core/hle/service/am/service/display_controller.h"
|
#include "core/hle/service/am/service/display_controller.h"
|
||||||
#include "core/hle/service/cmif_serialization.h"
|
#include "core/hle/service/cmif_serialization.h"
|
||||||
#include "core/hle/service/vi/shared_buffer_manager.h"
|
|
||||||
|
|
||||||
namespace Service::AM {
|
namespace Service::AM {
|
||||||
|
|
||||||
@@ -72,16 +71,16 @@ Result IDisplayController::TakeScreenShotOfOwnLayer(bool unknown0, s32 fbshare_l
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result IDisplayController::ClearCaptureBuffer(bool unknown0, s32 fbshare_layer_index, u32 color) {
|
Result IDisplayController::ClearCaptureBuffer(bool unknown0, s32 fbshare_layer_index, u32 color) {
|
||||||
LOG_DEBUG(Service_AM, "called, unknown0={} fbshare_layer_index={} color={:#x}", unknown0,
|
LOG_WARNING(Service_AM, "(STUBBED) called, unknown0={} fbshare_layer_index={} color={:#x}",
|
||||||
fbshare_layer_index, color);
|
unknown0, fbshare_layer_index, color);
|
||||||
R_RETURN(applet->display_layer_manager.ClearAppletCaptureBuffer(fbshare_layer_index, color));
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result IDisplayController::AcquireLastForegroundCaptureSharedBuffer(
|
Result IDisplayController::AcquireLastForegroundCaptureSharedBuffer(
|
||||||
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
||||||
LOG_DEBUG(Service_AM, "called");
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(
|
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(out_was_written,
|
||||||
out_was_written, out_fbshare_layer_index, VI::CaptureKind::LastForeground));
|
out_fbshare_layer_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
Result IDisplayController::ReleaseLastForegroundCaptureSharedBuffer() {
|
Result IDisplayController::ReleaseLastForegroundCaptureSharedBuffer() {
|
||||||
@@ -91,9 +90,9 @@ Result IDisplayController::ReleaseLastForegroundCaptureSharedBuffer() {
|
|||||||
|
|
||||||
Result IDisplayController::AcquireCallerAppletCaptureSharedBuffer(
|
Result IDisplayController::AcquireCallerAppletCaptureSharedBuffer(
|
||||||
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
||||||
LOG_DEBUG(Service_AM, "called");
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(
|
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(out_was_written,
|
||||||
out_was_written, out_fbshare_layer_index, VI::CaptureKind::CallerApplet));
|
out_fbshare_layer_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
Result IDisplayController::ReleaseCallerAppletCaptureSharedBuffer() {
|
Result IDisplayController::ReleaseCallerAppletCaptureSharedBuffer() {
|
||||||
@@ -103,9 +102,9 @@ Result IDisplayController::ReleaseCallerAppletCaptureSharedBuffer() {
|
|||||||
|
|
||||||
Result IDisplayController::AcquireLastApplicationCaptureSharedBuffer(
|
Result IDisplayController::AcquireLastApplicationCaptureSharedBuffer(
|
||||||
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
Out<bool> out_was_written, Out<s32> out_fbshare_layer_index) {
|
||||||
LOG_DEBUG(Service_AM, "called");
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(
|
R_RETURN(applet->display_layer_manager.WriteAppletCaptureBuffer(out_was_written,
|
||||||
out_was_written, out_fbshare_layer_index, VI::CaptureKind::LastApplication));
|
out_fbshare_layer_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
Result IDisplayController::ReleaseLastApplicationCaptureSharedBuffer() {
|
Result IDisplayController::ReleaseLastApplicationCaptureSharedBuffer() {
|
||||||
|
|||||||
@@ -4,15 +4,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#include "common/assert.h"
|
|
||||||
#include "common/logging.h"
|
|
||||||
#include "common/scratch_buffer.h"
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/kernel/k_page_group.h"
|
|
||||||
#include "core/hle/kernel/k_process.h"
|
#include "core/hle/kernel/k_process.h"
|
||||||
#include "core/hle/kernel/k_system_resource.h"
|
#include "core/hle/kernel/k_system_resource.h"
|
||||||
#include "core/hle/service/nvdrv/devices/nvmap.h"
|
#include "core/hle/service/nvdrv/devices/nvmap.h"
|
||||||
@@ -53,7 +47,7 @@ Result AllocateSharedBufferMemory(std::unique_ptr<Kernel::KPageGroup>* out_page_
|
|||||||
u32* end = system.DeviceMemory().GetPointer<u32>(block.GetAddress() + block.GetSize());
|
u32* end = system.DeviceMemory().GetPointer<u32>(block.GetAddress() + block.GetSize());
|
||||||
|
|
||||||
for (; start < end; start++) {
|
for (; start < end; start++) {
|
||||||
*start = 0x00000000;
|
*start = 0xFF0000FF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +168,7 @@ constexpr u32 SharedBufferBlockLinearWidth = 1280;
|
|||||||
constexpr u32 SharedBufferBlockLinearHeight = 768;
|
constexpr u32 SharedBufferBlockLinearHeight = 768;
|
||||||
constexpr u32 SharedBufferBlockLinearStride =
|
constexpr u32 SharedBufferBlockLinearStride =
|
||||||
SharedBufferBlockLinearWidth * SharedBufferBlockLinearBpp;
|
SharedBufferBlockLinearWidth * SharedBufferBlockLinearBpp;
|
||||||
|
constexpr u32 SharedBufferNumSlots = 7;
|
||||||
constexpr u32 SharedBufferNumCaptureSlots = 3;
|
|
||||||
constexpr u32 SharedBufferSlotsPerSession = 2;
|
|
||||||
constexpr u32 SharedBufferMaxSessions = 2;
|
|
||||||
|
|
||||||
constexpr u32 SharedBufferNumSlots =
|
|
||||||
SharedBufferNumCaptureSlots + SharedBufferSlotsPerSession * SharedBufferMaxSessions;
|
|
||||||
static_assert(SharedBufferNumSlots <= 16, "Shared buffer pool exceeds the maximum texture count");
|
|
||||||
|
|
||||||
constexpr u32 SharedBufferWidth = 1280;
|
constexpr u32 SharedBufferWidth = 1280;
|
||||||
constexpr u32 SharedBufferHeight = 720;
|
constexpr u32 SharedBufferHeight = 720;
|
||||||
@@ -205,52 +192,7 @@ constexpr SharedMemoryPoolLayout SharedBufferPoolLayout = [] {
|
|||||||
return layout;
|
return layout;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
constexpr u32 GetCaptureSlot(CaptureKind kind) {
|
void MakeGraphicBuffer(android::BufferQueueProducer& producer, u32 slot, u32 handle) {
|
||||||
return static_cast<u32>(kind);
|
|
||||||
}
|
|
||||||
static_assert(static_cast<u32>(CaptureKind::CallerApplet) + 1 == SharedBufferNumCaptureSlots,
|
|
||||||
"Capture slot count does not match CaptureKind");
|
|
||||||
|
|
||||||
constexpr u32 GetPresentationSlot(u32 slot_base, u32 index) {
|
|
||||||
return SharedBufferNumCaptureSlots + slot_base + index;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr u32 ColorOpaqueBlackRgba32 = 0xFF000000;
|
|
||||||
|
|
||||||
template <typename F>
|
|
||||||
void ForEachPoolChunk(Core::System& system, Kernel::KPageGroup& page_group, u64 offset, u64 size,
|
|
||||||
F&& writer) {
|
|
||||||
Common::ScratchBuffer<u32> scratch;
|
|
||||||
const u64 range_end = offset + size;
|
|
||||||
u64 pool_pos = 0;
|
|
||||||
|
|
||||||
for (auto& block : page_group) {
|
|
||||||
const u64 block_begin = pool_pos;
|
|
||||||
const u64 block_end = block_begin + block.GetSize();
|
|
||||||
pool_pos = block_end;
|
|
||||||
|
|
||||||
if (block_end <= offset) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (block_begin >= range_end) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const u64 chunk_begin = (std::max)(block_begin, offset);
|
|
||||||
const u64 chunk_end = (std::min)(block_end, range_end);
|
|
||||||
const u64 chunk_size = chunk_end - chunk_begin;
|
|
||||||
|
|
||||||
u8* const dst =
|
|
||||||
system.DeviceMemory().GetPointer<u8>(block.GetAddress()) + (chunk_begin - block_begin);
|
|
||||||
|
|
||||||
writer(dst, chunk_begin - offset, chunk_size);
|
|
||||||
|
|
||||||
system.GPU().Host1x().MemoryManager().ApplyOpOnPointer(
|
|
||||||
dst, scratch, [&](DAddr addr) { system.GPU().InvalidateRegion(addr, chunk_size); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MakeGraphicBuffer(android::BufferQueueProducer& producer, u32 producer_slot, u32 pool_slot, u32 handle) {
|
|
||||||
auto buffer = std::make_shared<android::NvGraphicBuffer>();
|
auto buffer = std::make_shared<android::NvGraphicBuffer>();
|
||||||
buffer->width = SharedBufferWidth;
|
buffer->width = SharedBufferWidth;
|
||||||
buffer->height = SharedBufferHeight;
|
buffer->height = SharedBufferHeight;
|
||||||
@@ -258,8 +200,8 @@ void MakeGraphicBuffer(android::BufferQueueProducer& producer, u32 producer_slot
|
|||||||
buffer->format = SharedBufferBlockLinearFormat;
|
buffer->format = SharedBufferBlockLinearFormat;
|
||||||
buffer->external_format = SharedBufferBlockLinearFormat;
|
buffer->external_format = SharedBufferBlockLinearFormat;
|
||||||
buffer->buffer_id = handle;
|
buffer->buffer_id = handle;
|
||||||
buffer->offset = pool_slot * SharedBufferSlotSize;
|
buffer->offset = slot * SharedBufferSlotSize;
|
||||||
ASSERT(producer.SetPreallocatedBuffer(producer_slot, buffer) == android::Status::NoError);
|
ASSERT(producer.SetPreallocatedBuffer(slot, buffer) == android::Status::NoError);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -273,91 +215,59 @@ SharedBufferManager::~SharedBufferManager() = default;
|
|||||||
Result SharedBufferManager::CreateSession(Kernel::KProcess* owner_process, u64* out_buffer_id,
|
Result SharedBufferManager::CreateSession(Kernel::KProcess* owner_process, u64* out_buffer_id,
|
||||||
u64* out_layer_handle, u64 display_id,
|
u64* out_layer_handle, u64 display_id,
|
||||||
bool enable_blending) {
|
bool enable_blending) {
|
||||||
{
|
std::scoped_lock lk{m_guard};
|
||||||
std::scoped_lock lk{m_guard};
|
|
||||||
|
|
||||||
// Ensure we haven't already created.
|
// Ensure we haven't already created.
|
||||||
const u64 aruid = owner_process->GetProcessId();
|
const u64 aruid = owner_process->GetProcessId();
|
||||||
R_UNLESS(!m_sessions.contains(aruid), VI::ResultPermissionDenied);
|
R_UNLESS(!m_sessions.contains(aruid), VI::ResultPermissionDenied);
|
||||||
|
|
||||||
// Allocate memory for the shared buffer if needed.
|
// Allocate memory for the shared buffer if needed.
|
||||||
if (!m_buffer_page_group) {
|
if (!m_buffer_page_group) {
|
||||||
R_TRY(AllocateSharedBufferMemory(std::addressof(m_buffer_page_group), m_system,
|
R_TRY(AllocateSharedBufferMemory(std::addressof(m_buffer_page_group), m_system,
|
||||||
SharedBufferSize));
|
SharedBufferSize));
|
||||||
|
|
||||||
// Record buffer id.
|
// Record buffer id.
|
||||||
m_buffer_id = m_next_buffer_id++;
|
m_buffer_id = m_next_buffer_id++;
|
||||||
|
|
||||||
// Record display id.
|
// Record display id.
|
||||||
m_display_id = display_id;
|
m_display_id = display_id;
|
||||||
|
|
||||||
for (u32 slot = 0; slot < SharedBufferNumCaptureSlots; slot++) {
|
|
||||||
ForEachPoolChunk(m_system, *m_buffer_page_group, u64{slot} * SharedBufferSlotSize,
|
|
||||||
SharedBufferSlotSize, [](u8* dst, u64, u64 length) {
|
|
||||||
std::fill_n(reinterpret_cast<u32*>(dst), length / sizeof(u32),
|
|
||||||
ColorOpaqueBlackRgba32);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Claim a presentation slot range.
|
|
||||||
u32 slot_base = 0;
|
|
||||||
|
|
||||||
std::array<bool, SharedBufferMaxSessions> in_use{};
|
|
||||||
for (const auto& [existing_aruid, existing] : m_sessions) {
|
|
||||||
const u32 index = existing.presentation_slot_base / SharedBufferSlotsPerSession;
|
|
||||||
|
|
||||||
if (index < in_use.size())
|
|
||||||
in_use[index] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
u32 index = 0;
|
|
||||||
while (index < in_use.size() && in_use[index])
|
|
||||||
index++;
|
|
||||||
|
|
||||||
if (index >= in_use.size()) {
|
|
||||||
LOG_ERROR(Service_VI, "Out of shared buffer presentation slots ({} sessions)", SharedBufferMaxSessions);
|
|
||||||
R_THROW(VI::ResultOperationFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
slot_base = index * SharedBufferSlotsPerSession;
|
|
||||||
|
|
||||||
// Map into process.
|
|
||||||
Common::ProcessAddress map_address{};
|
|
||||||
R_TRY(MapSharedBufferIntoProcessAddressSpace(std::addressof(map_address), m_buffer_page_group,
|
|
||||||
owner_process, m_system));
|
|
||||||
|
|
||||||
// Create new session.
|
|
||||||
auto [it, was_emplaced] = m_sessions.emplace(aruid, SharedBufferSession{});
|
|
||||||
auto& session = it->second;
|
|
||||||
session.presentation_slot_base = slot_base;
|
|
||||||
|
|
||||||
auto& container = m_nvdrv->GetContainer();
|
|
||||||
session.session_id = container.OpenSession(owner_process);
|
|
||||||
session.nvmap_fd = m_nvdrv->Open("/dev/nvmap", session.session_id);
|
|
||||||
|
|
||||||
// Create an nvmap handle for the buffer and assign the memory to it.
|
|
||||||
R_TRY(AllocateHandleForBuffer(std::addressof(session.buffer_nvmap_handle), *m_nvdrv,
|
|
||||||
session.nvmap_fd, map_address, SharedBufferSize));
|
|
||||||
|
|
||||||
// Create and open a layer for the display.
|
|
||||||
s32 producer_binder_id;
|
|
||||||
R_TRY(m_container.CreateStrayLayer(std::addressof(producer_binder_id),
|
|
||||||
std::addressof(session.layer_id), display_id));
|
|
||||||
|
|
||||||
// Configure blending and z-index
|
|
||||||
R_ASSERT(m_container.SetLayerBlending(session.layer_id, enable_blending));
|
|
||||||
|
|
||||||
// Get the producer and set preallocated buffers.
|
|
||||||
std::shared_ptr<android::BufferQueueProducer> producer;
|
|
||||||
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), session.layer_id));
|
|
||||||
for (u32 i = 0; i < SharedBufferSlotsPerSession; i++)
|
|
||||||
MakeGraphicBuffer(*producer, i, GetPresentationSlot(session.presentation_slot_base, i), session.buffer_nvmap_handle);
|
|
||||||
|
|
||||||
// Assign outputs.
|
|
||||||
*out_buffer_id = m_buffer_id;
|
|
||||||
*out_layer_handle = session.layer_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map into process.
|
||||||
|
Common::ProcessAddress map_address{};
|
||||||
|
R_TRY(MapSharedBufferIntoProcessAddressSpace(std::addressof(map_address), m_buffer_page_group,
|
||||||
|
owner_process, m_system));
|
||||||
|
|
||||||
|
// Create new session.
|
||||||
|
auto [it, was_emplaced] = m_sessions.emplace(aruid, SharedBufferSession{});
|
||||||
|
auto& session = it->second;
|
||||||
|
|
||||||
|
auto& container = m_nvdrv->GetContainer();
|
||||||
|
session.session_id = container.OpenSession(owner_process);
|
||||||
|
session.nvmap_fd = m_nvdrv->Open("/dev/nvmap", session.session_id);
|
||||||
|
|
||||||
|
// Create an nvmap handle for the buffer and assign the memory to it.
|
||||||
|
R_TRY(AllocateHandleForBuffer(std::addressof(session.buffer_nvmap_handle), *m_nvdrv,
|
||||||
|
session.nvmap_fd, map_address, SharedBufferSize));
|
||||||
|
|
||||||
|
// Create and open a layer for the display.
|
||||||
|
s32 producer_binder_id;
|
||||||
|
R_TRY(m_container.CreateStrayLayer(std::addressof(producer_binder_id),
|
||||||
|
std::addressof(session.layer_id), display_id));
|
||||||
|
|
||||||
|
// Configure blending and z-index
|
||||||
|
R_ASSERT(m_container.SetLayerBlending(session.layer_id, enable_blending));
|
||||||
|
|
||||||
|
// Get the producer and set preallocated buffers.
|
||||||
|
std::shared_ptr<android::BufferQueueProducer> producer;
|
||||||
|
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), session.layer_id));
|
||||||
|
MakeGraphicBuffer(*producer, 0, session.buffer_nvmap_handle);
|
||||||
|
MakeGraphicBuffer(*producer, 1, session.buffer_nvmap_handle);
|
||||||
|
|
||||||
|
// Assign outputs.
|
||||||
|
*out_buffer_id = m_buffer_id;
|
||||||
|
*out_layer_handle = session.layer_id;
|
||||||
|
|
||||||
// We succeeded.
|
// We succeeded.
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
@@ -426,51 +336,14 @@ Result SharedBufferManager::AcquireSharedFrameBuffer(android::Fence* out_fence,
|
|||||||
SharedBufferBlockLinearFormat, 0) == android::Status::NoError,
|
SharedBufferBlockLinearFormat, 0) == android::Status::NoError,
|
||||||
VI::ResultOperationFailed);
|
VI::ResultOperationFailed);
|
||||||
|
|
||||||
out_slot_indexes.fill(-1);
|
// Assign remaining outputs.
|
||||||
|
*out_target_slot = slot;
|
||||||
{
|
out_slot_indexes = {0, 1, -1, -1};
|
||||||
std::scoped_lock lk{m_guard};
|
|
||||||
const auto* const session = this->FindSessionByLayerIdLocked(layer_id);
|
|
||||||
if (session == nullptr) {
|
|
||||||
producer->CancelBuffer(slot, *out_fence);
|
|
||||||
// LOG_DEBUG(Service_VI, "No Session found");
|
|
||||||
R_THROW(VI::ResultNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (u32 i = 0; i < SharedBufferSlotsPerSession; i++)
|
|
||||||
out_slot_indexes[i] =
|
|
||||||
static_cast<s32>(GetPresentationSlot(session->presentation_slot_base, i));
|
|
||||||
|
|
||||||
*out_target_slot = static_cast<s64>(
|
|
||||||
GetPresentationSlot(session->presentation_slot_base, static_cast<u32>(slot)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We succeeded.
|
// We succeeded.
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SharedBufferManager::GetProducerSlotLocked(s32* out_producer_slot, u64 layer_id,
|
|
||||||
s64 pool_slot) const {
|
|
||||||
const auto* const session = this->FindSessionByLayerIdLocked(layer_id);
|
|
||||||
R_UNLESS(session != nullptr, VI::ResultNotFound);
|
|
||||||
|
|
||||||
const s64 base = GetPresentationSlot(session->presentation_slot_base, 0);
|
|
||||||
const s64 producer_slot = pool_slot - base;
|
|
||||||
R_UNLESS(producer_slot >= 0 && producer_slot < SharedBufferSlotsPerSession,
|
|
||||||
VI::ResultOperationFailed);
|
|
||||||
|
|
||||||
*out_producer_slot = static_cast<s32>(producer_slot);
|
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharedBufferSession* SharedBufferManager::FindSessionByLayerIdLocked(u64 layer_id) const {
|
|
||||||
for (const auto& [aruid, session] : m_sessions)
|
|
||||||
if (session.layer_id == layer_id)
|
|
||||||
return std::addressof(session);
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result SharedBufferManager::PresentSharedFrameBuffer(android::Fence fence,
|
Result SharedBufferManager::PresentSharedFrameBuffer(android::Fence fence,
|
||||||
Common::Rectangle<s32> crop_region,
|
Common::Rectangle<s32> crop_region,
|
||||||
u32 transform, s32 swap_interval, u64 layer_id,
|
u32 transform, s32 swap_interval, u64 layer_id,
|
||||||
@@ -479,20 +352,14 @@ Result SharedBufferManager::PresentSharedFrameBuffer(android::Fence fence,
|
|||||||
std::shared_ptr<android::BufferQueueProducer> producer;
|
std::shared_ptr<android::BufferQueueProducer> producer;
|
||||||
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), layer_id));
|
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), layer_id));
|
||||||
|
|
||||||
s32 producer_slot;
|
|
||||||
{
|
|
||||||
std::scoped_lock lk{m_guard};
|
|
||||||
R_TRY(this->GetProducerSlotLocked(std::addressof(producer_slot), layer_id, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request to queue the buffer.
|
// Request to queue the buffer.
|
||||||
std::shared_ptr<android::GraphicBuffer> buffer;
|
std::shared_ptr<android::GraphicBuffer> buffer;
|
||||||
R_UNLESS(producer->RequestBuffer(producer_slot, std::addressof(buffer)) ==
|
R_UNLESS(producer->RequestBuffer(static_cast<s32>(slot), std::addressof(buffer)) ==
|
||||||
android::Status::NoError,
|
android::Status::NoError,
|
||||||
VI::ResultOperationFailed);
|
VI::ResultOperationFailed);
|
||||||
|
|
||||||
ON_RESULT_FAILURE {
|
ON_RESULT_FAILURE {
|
||||||
producer->CancelBuffer(producer_slot, fence);
|
producer->CancelBuffer(static_cast<s32>(slot), fence);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Queue the buffer to the producer.
|
// Queue the buffer to the producer.
|
||||||
@@ -502,10 +369,12 @@ Result SharedBufferManager::PresentSharedFrameBuffer(android::Fence fence,
|
|||||||
input.fence = fence;
|
input.fence = fence;
|
||||||
input.transform = static_cast<android::NativeWindowTransform>(transform);
|
input.transform = static_cast<android::NativeWindowTransform>(transform);
|
||||||
input.swap_interval = swap_interval;
|
input.swap_interval = swap_interval;
|
||||||
R_UNLESS(producer->QueueBuffer(producer_slot, input, std::addressof(output)) ==
|
R_UNLESS(producer->QueueBuffer(static_cast<s32>(slot), input, std::addressof(output)) ==
|
||||||
android::Status::NoError,
|
android::Status::NoError,
|
||||||
VI::ResultOperationFailed);
|
VI::ResultOperationFailed);
|
||||||
|
|
||||||
|
(void)m_container.SetLayerZIndex(layer_id, 100000);
|
||||||
|
|
||||||
// We succeeded.
|
// We succeeded.
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
@@ -515,14 +384,8 @@ Result SharedBufferManager::CancelSharedFrameBuffer(u64 layer_id, s64 slot) {
|
|||||||
std::shared_ptr<android::BufferQueueProducer> producer;
|
std::shared_ptr<android::BufferQueueProducer> producer;
|
||||||
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), layer_id));
|
R_TRY(m_container.GetLayerProducerHandle(std::addressof(producer), layer_id));
|
||||||
|
|
||||||
s32 producer_slot;
|
|
||||||
{
|
|
||||||
std::scoped_lock lk{m_guard};
|
|
||||||
R_TRY(this->GetProducerSlotLocked(std::addressof(producer_slot), layer_id, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel.
|
// Cancel.
|
||||||
producer->CancelBuffer(producer_slot, android::Fence::NoFence());
|
producer->CancelBuffer(static_cast<s32>(slot), android::Fence::NoFence());
|
||||||
|
|
||||||
// We succeeded.
|
// We succeeded.
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
@@ -541,47 +404,31 @@ Result SharedBufferManager::GetSharedFrameBufferAcquirableEvent(Kernel::KReadabl
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SharedBufferManager::WriteAppletCaptureBuffer(bool* out_was_written, s32* out_layer_index, CaptureKind kind) {
|
Result SharedBufferManager::WriteAppletCaptureBuffer(bool* out_was_written, s32* out_layer_index) {
|
||||||
std::scoped_lock lk{m_guard};
|
std::vector<u8> capture_buffer(m_system.GPU().GetAppletCaptureBuffer());
|
||||||
R_UNLESS(m_buffer_page_group != nullptr, VI::ResultNotFound);
|
Common::ScratchBuffer<u32> scratch;
|
||||||
|
|
||||||
const std::vector<u8> capture = m_system.GPU().GetAppletCaptureBuffer();
|
// TODO: this could be optimized
|
||||||
const u32 slot = GetCaptureSlot(kind);
|
s64 e = -1280 * 768 * 4;
|
||||||
|
for (auto& block : *m_buffer_page_group) {
|
||||||
|
u8* start = m_system.DeviceMemory().GetPointer<u8>(block.GetAddress());
|
||||||
|
u8* end = m_system.DeviceMemory().GetPointer<u8>(block.GetAddress() + block.GetSize());
|
||||||
|
|
||||||
if (capture.size() < SharedBufferSlotSize) {
|
for (; start < end; start++) {
|
||||||
//LOG_WARNING(Service_VI, "Capture buffer is {} bytes, expected at least {}; not writing",
|
*start = 0;
|
||||||
// capture.size(), SharedBufferSlotSize);
|
if (e >= 0 && e < static_cast<s64>(capture_buffer.size())) {
|
||||||
*out_was_written = false;
|
*start = capture_buffer[e];
|
||||||
*out_layer_index = static_cast<s32>(slot);
|
}
|
||||||
R_SUCCEED();
|
e++;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_system.GPU().Host1x().MemoryManager().ApplyOpOnPointer(start, scratch, [&](DAddr addr) {
|
||||||
|
m_system.GPU().InvalidateRegion(addr, end - start);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEachPoolChunk(m_system, *m_buffer_page_group, u64{slot} * SharedBufferSlotSize,
|
|
||||||
SharedBufferSlotSize,
|
|
||||||
[&](u8* dst, u64 src_offset, u64 length) {
|
|
||||||
std::memcpy(dst, capture.data() + src_offset, length);
|
|
||||||
});
|
|
||||||
|
|
||||||
*out_was_written = true;
|
*out_was_written = true;
|
||||||
*out_layer_index = static_cast<s32>(slot);
|
*out_layer_index = 1;
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result SharedBufferManager::ClearAppletCaptureBuffer(s32 layer_index, u32 color) {
|
|
||||||
std::scoped_lock lk{m_guard};
|
|
||||||
R_UNLESS(m_buffer_page_group != nullptr, VI::ResultNotFound);
|
|
||||||
|
|
||||||
if (layer_index < 0 || layer_index >= static_cast<s32>(SharedBufferNumCaptureSlots)) {
|
|
||||||
LOG_WARNING(Service_VI, "Couldnt clear non-capture slot {}", layer_index);
|
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEachPoolChunk(m_system, *m_buffer_page_group, u64{static_cast<u32>(layer_index)} * SharedBufferSlotSize,
|
|
||||||
SharedBufferSlotSize, [&](u8* dst, u64 src_offset, u64 length) {
|
|
||||||
ASSERT(src_offset % sizeof(u32) == 0 && length % sizeof(u32) == 0);
|
|
||||||
std::fill_n(reinterpret_cast<u32*>(dst), length / sizeof(u32), color);
|
|
||||||
});
|
|
||||||
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
@@ -48,12 +48,6 @@ static_assert(sizeof(SharedMemoryPoolLayout) == 0x188, "SharedMemoryPoolLayout h
|
|||||||
|
|
||||||
struct SharedBufferSession;
|
struct SharedBufferSession;
|
||||||
|
|
||||||
enum class CaptureKind : u32 {
|
|
||||||
LastApplication,
|
|
||||||
LastForeground,
|
|
||||||
CallerApplet,
|
|
||||||
};
|
|
||||||
|
|
||||||
class SharedBufferManager final {
|
class SharedBufferManager final {
|
||||||
public:
|
public:
|
||||||
explicit SharedBufferManager(Core::System& system, Container& container,
|
explicit SharedBufferManager(Core::System& system, Container& container,
|
||||||
@@ -74,15 +68,9 @@ public:
|
|||||||
Result CancelSharedFrameBuffer(u64 layer_id, s64 slot);
|
Result CancelSharedFrameBuffer(u64 layer_id, s64 slot);
|
||||||
Result GetSharedFrameBufferAcquirableEvent(Kernel::KReadableEvent** out_event, u64 layer_id);
|
Result GetSharedFrameBufferAcquirableEvent(Kernel::KReadableEvent** out_event, u64 layer_id);
|
||||||
|
|
||||||
Result WriteAppletCaptureBuffer(bool* out_was_written, s32* out_layer_index, CaptureKind kind);
|
Result WriteAppletCaptureBuffer(bool* out_was_written, s32* out_layer_index);
|
||||||
Result ClearAppletCaptureBuffer(s32 layer_index, u32 color);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const SharedBufferSession* FindSessionByLayerIdLocked(u64 layer_id) const;
|
|
||||||
|
|
||||||
/// Converts a pool slot index, which is what the guest works in, back to the buffer queues slot index
|
|
||||||
Result GetProducerSlotLocked(s32* out_producer_slot, u64 layer_id, s64 pool_slot) const;
|
|
||||||
|
|
||||||
u64 m_next_buffer_id = 1;
|
u64 m_next_buffer_id = 1;
|
||||||
u64 m_display_id = 0;
|
u64 m_display_id = 0;
|
||||||
u64 m_buffer_id = 0;
|
u64 m_buffer_id = 0;
|
||||||
@@ -101,7 +89,6 @@ struct SharedBufferSession {
|
|||||||
Nvidia::NvCore::SessionId session_id = {};
|
Nvidia::NvCore::SessionId session_id = {};
|
||||||
u64 layer_id = {};
|
u64 layer_id = {};
|
||||||
u32 buffer_nvmap_handle = 0;
|
u32 buffer_nvmap_handle = 0;
|
||||||
u32 presentation_slot_base = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Service::VI
|
} // namespace Service::VI
|
||||||
|
|||||||
Reference in New Issue
Block a user