2022-04-23 04:59:50 -04:00
|
|
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
2021-11-11 19:15:51 -08:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
2018-01-22 11:54:58 -05:00
|
|
|
|
|
|
|
#include <algorithm>
|
2018-10-30 05:03:25 +01:00
|
|
|
#include <optional>
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2018-08-07 08:24:30 -04:00
|
|
|
#include "common/assert.h"
|
|
|
|
#include "common/logging/log.h"
|
2018-04-18 20:28:50 -04:00
|
|
|
#include "common/microprofile.h"
|
2018-01-22 11:54:58 -05:00
|
|
|
#include "common/scope_exit.h"
|
2021-04-14 16:07:40 -07:00
|
|
|
#include "common/settings.h"
|
2020-05-29 15:00:17 -04:00
|
|
|
#include "common/thread.h"
|
2018-02-14 10:16:39 -07:00
|
|
|
#include "core/core.h"
|
2018-01-22 11:54:58 -05:00
|
|
|
#include "core/core_timing.h"
|
2021-01-29 22:48:06 -08:00
|
|
|
#include "core/hle/kernel/k_readable_event.h"
|
2018-01-22 11:54:58 -05:00
|
|
|
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
|
|
|
|
#include "core/hle/service/nvdrv/nvdrv.h"
|
2023-02-19 15:05:34 -05:00
|
|
|
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
|
|
|
|
#include "core/hle/service/nvnflinger/buffer_queue_core.h"
|
2023-09-28 23:45:49 -06:00
|
|
|
#include "core/hle/service/nvnflinger/fb_share_buffer_manager.h"
|
2024-01-22 12:40:50 -05:00
|
|
|
#include "core/hle/service/nvnflinger/hardware_composer.h"
|
2023-02-19 15:05:34 -05:00
|
|
|
#include "core/hle/service/nvnflinger/hos_binder_driver_server.h"
|
|
|
|
#include "core/hle/service/nvnflinger/nvnflinger.h"
|
|
|
|
#include "core/hle/service/nvnflinger/ui/graphic_buffer.h"
|
2019-02-19 17:00:03 -05:00
|
|
|
#include "core/hle/service/vi/display/vi_display.h"
|
|
|
|
#include "core/hle/service/vi/layer/vi_layer.h"
|
2022-09-25 21:20:36 -04:00
|
|
|
#include "core/hle/service/vi/vi_results.h"
|
2021-10-02 00:39:57 -04:00
|
|
|
#include "video_core/gpu.h"
|
2022-01-30 10:31:13 +01:00
|
|
|
#include "video_core/host1x/host1x.h"
|
|
|
|
#include "video_core/host1x/syncpoint_manager.h"
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
namespace Service::Nvnflinger {
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2020-07-15 18:30:06 -04:00
|
|
|
constexpr auto frame_ns = std::chrono::nanoseconds{1000000000 / 60};
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
void Nvnflinger::SplitVSync(std::stop_token stop_token) {
|
2020-05-29 15:00:17 -04:00
|
|
|
system.RegisterHostThread();
|
2022-10-03 18:43:56 -04:00
|
|
|
std::string name = "VSyncThread";
|
2020-05-29 15:00:17 -04:00
|
|
|
MicroProfileOnThreadCreate(name.c_str());
|
2020-12-30 01:34:50 -08:00
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
SCOPE_EXIT({ MicroProfileOnThreadExit(); });
|
|
|
|
|
2020-05-29 15:00:17 -04:00
|
|
|
Common::SetCurrentThreadName(name.c_str());
|
|
|
|
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
|
2022-07-26 22:04:18 +01:00
|
|
|
|
2021-10-02 00:39:57 -04:00
|
|
|
while (!stop_token.stop_requested()) {
|
2023-06-20 11:41:38 -04:00
|
|
|
vsync_signal.Wait();
|
2022-07-26 22:04:18 +01:00
|
|
|
|
2023-06-20 11:41:38 -04:00
|
|
|
const auto lock_guard = Lock();
|
2023-10-29 23:38:24 -04:00
|
|
|
|
|
|
|
if (!is_abandoned) {
|
|
|
|
Compose();
|
|
|
|
}
|
2020-05-29 15:00:17 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
Nvnflinger::Nvnflinger(Core::System& system_, HosBinderDriverServer& hos_binder_driver_server_)
|
|
|
|
: system(system_), service_context(system_, "nvnflinger"),
|
2021-11-11 19:15:51 -08:00
|
|
|
hos_binder_driver_server(hos_binder_driver_server_) {
|
|
|
|
displays.emplace_back(0, "Default", hos_binder_driver_server, service_context, system);
|
|
|
|
displays.emplace_back(1, "External", hos_binder_driver_server, service_context, system);
|
|
|
|
displays.emplace_back(2, "Edid", hos_binder_driver_server, service_context, system);
|
|
|
|
displays.emplace_back(3, "Internal", hos_binder_driver_server, service_context, system);
|
|
|
|
displays.emplace_back(4, "Null", hos_binder_driver_server, service_context, system);
|
2020-02-27 10:47:02 -04:00
|
|
|
guard = std::make_shared<std::mutex>();
|
2019-02-21 10:43:26 -05:00
|
|
|
|
2018-01-22 11:54:58 -05:00
|
|
|
// Schedule the screen composition events
|
2022-07-26 22:04:18 +01:00
|
|
|
multi_composition_event = Core::Timing::CreateEvent(
|
|
|
|
"ScreenComposition",
|
2023-12-23 13:58:09 -05:00
|
|
|
[this](s64 time,
|
2022-07-26 22:04:18 +01:00
|
|
|
std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> {
|
2023-06-20 11:41:38 -04:00
|
|
|
vsync_signal.Set();
|
2022-07-26 22:04:18 +01:00
|
|
|
return std::chrono::nanoseconds(GetNextTicks());
|
|
|
|
});
|
|
|
|
|
|
|
|
single_composition_event = Core::Timing::CreateEvent(
|
2022-07-10 06:59:40 +01:00
|
|
|
"ScreenComposition",
|
2023-12-23 13:58:09 -05:00
|
|
|
[this](s64 time,
|
2022-07-10 06:59:40 +01:00
|
|
|
std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> {
|
2021-04-26 09:11:33 -04:00
|
|
|
const auto lock_guard = Lock();
|
2019-09-22 16:41:34 +10:00
|
|
|
Compose();
|
2020-07-15 18:30:06 -04:00
|
|
|
|
2022-07-26 22:04:18 +01:00
|
|
|
return std::chrono::nanoseconds(GetNextTicks());
|
2019-09-22 16:41:34 +10:00
|
|
|
});
|
2020-07-15 18:30:06 -04:00
|
|
|
|
2020-05-29 15:00:17 -04:00
|
|
|
if (system.IsMulticore()) {
|
2022-07-26 22:04:18 +01:00
|
|
|
system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, multi_composition_event);
|
2021-10-02 00:39:57 -04:00
|
|
|
vsync_thread = std::jthread([this](std::stop_token token) { SplitVSync(token); });
|
2020-05-29 15:00:17 -04:00
|
|
|
} else {
|
2022-07-26 22:04:18 +01:00
|
|
|
system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, single_composition_event);
|
2020-05-29 15:00:17 -04:00
|
|
|
}
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
Nvnflinger::~Nvnflinger() {
|
2022-07-26 22:04:18 +01:00
|
|
|
if (system.IsMulticore()) {
|
2023-12-23 13:58:09 -05:00
|
|
|
system.CoreTiming().UnscheduleEvent(multi_composition_event);
|
2022-07-26 22:04:18 +01:00
|
|
|
vsync_thread.request_stop();
|
2023-06-20 11:41:38 -04:00
|
|
|
vsync_signal.Set();
|
2022-07-26 22:04:18 +01:00
|
|
|
} else {
|
2023-12-23 13:58:09 -05:00
|
|
|
system.CoreTiming().UnscheduleEvent(single_composition_event);
|
2020-05-29 15:00:17 -04:00
|
|
|
}
|
2021-11-11 19:15:51 -08:00
|
|
|
|
2022-10-23 05:45:45 -04:00
|
|
|
ShutdownLayers();
|
|
|
|
|
|
|
|
if (nvdrv) {
|
|
|
|
nvdrv->Close(disp_fd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
void Nvnflinger::ShutdownLayers() {
|
2023-10-29 23:38:24 -04:00
|
|
|
// Abandon consumers.
|
|
|
|
{
|
|
|
|
const auto lock_guard = Lock();
|
|
|
|
for (auto& display : displays) {
|
2024-01-17 18:45:39 -05:00
|
|
|
display.Abandon();
|
2021-11-11 19:15:51 -08:00
|
|
|
}
|
2023-10-29 23:38:24 -04:00
|
|
|
|
|
|
|
is_abandoned = true;
|
2021-11-11 19:15:51 -08:00
|
|
|
}
|
2023-10-29 23:38:24 -04:00
|
|
|
|
|
|
|
// Join the vsync thread, if it exists.
|
|
|
|
vsync_thread = {};
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
void Nvnflinger::SetNVDrvInstance(std::shared_ptr<Nvidia::Module> instance) {
|
2018-08-07 09:17:09 -04:00
|
|
|
nvdrv = std::move(instance);
|
2024-01-15 21:47:59 -05:00
|
|
|
disp_fd = nvdrv->Open("/dev/nvdisp_disp0", {});
|
2018-08-07 09:17:09 -04:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
std::optional<u64> Nvnflinger::OpenDisplay(std::string_view name) {
|
2021-04-26 09:11:33 -04:00
|
|
|
const auto lock_guard = Lock();
|
2020-12-10 15:32:52 -08:00
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
LOG_DEBUG(Service_Nvnflinger, "Opening \"{}\" display", name);
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2019-02-21 10:43:26 -05:00
|
|
|
const auto itr =
|
|
|
|
std::find_if(displays.begin(), displays.end(),
|
|
|
|
[&](const VI::Display& display) { return display.GetName() == name; });
|
2019-04-06 23:02:55 +03:00
|
|
|
|
2019-02-05 16:20:04 -05:00
|
|
|
if (itr == displays.end()) {
|
2020-09-22 17:31:53 -04:00
|
|
|
return std::nullopt;
|
2019-02-05 16:20:04 -05:00
|
|
|
}
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2019-02-21 10:43:26 -05:00
|
|
|
return itr->GetID();
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
bool Nvnflinger::CloseDisplay(u64 display_id) {
|
2022-10-25 17:42:02 -04:00
|
|
|
const auto lock_guard = Lock();
|
|
|
|
auto* const display = FindDisplay(display_id);
|
|
|
|
|
|
|
|
if (display == nullptr) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
display->Reset();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-01-22 12:40:50 -05:00
|
|
|
std::optional<u64> Nvnflinger::CreateLayer(u64 display_id, LayerBlending blending) {
|
2021-04-26 09:11:33 -04:00
|
|
|
const auto lock_guard = Lock();
|
2019-02-05 16:20:04 -05:00
|
|
|
auto* const display = FindDisplay(display_id);
|
|
|
|
|
|
|
|
if (display == nullptr) {
|
2020-09-22 17:31:53 -04:00
|
|
|
return std::nullopt;
|
2019-02-05 16:20:04 -05:00
|
|
|
}
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2019-01-29 14:52:14 -05:00
|
|
|
const u64 layer_id = next_layer_id++;
|
2024-01-22 12:40:50 -05:00
|
|
|
CreateLayerAtId(*display, layer_id, blending);
|
2021-05-06 11:20:52 -04:00
|
|
|
return layer_id;
|
|
|
|
}
|
|
|
|
|
2024-01-22 12:40:50 -05:00
|
|
|
void Nvnflinger::CreateLayerAtId(VI::Display& display, u64 layer_id, LayerBlending blending) {
|
2021-11-11 19:15:51 -08:00
|
|
|
const auto buffer_id = next_buffer_queue_id++;
|
2022-06-27 12:39:57 +08:00
|
|
|
display.CreateLayer(layer_id, buffer_id, nvdrv->container);
|
2024-01-22 12:40:50 -05:00
|
|
|
display.FindLayer(layer_id)->SetBlending(blending);
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2024-01-17 22:03:40 -05:00
|
|
|
bool Nvnflinger::OpenLayer(u64 layer_id) {
|
2023-12-10 12:32:44 -05:00
|
|
|
const auto lock_guard = Lock();
|
|
|
|
|
|
|
|
for (auto& display : displays) {
|
|
|
|
if (auto* layer = display.FindLayer(layer_id); layer) {
|
2024-01-17 22:03:40 -05:00
|
|
|
return layer->Open();
|
2023-12-10 12:32:44 -05:00
|
|
|
}
|
|
|
|
}
|
2024-01-17 22:03:40 -05:00
|
|
|
|
|
|
|
return false;
|
2023-12-10 12:32:44 -05:00
|
|
|
}
|
|
|
|
|
2024-01-17 22:03:40 -05:00
|
|
|
bool Nvnflinger::CloseLayer(u64 layer_id) {
|
2021-04-26 09:11:33 -04:00
|
|
|
const auto lock_guard = Lock();
|
2020-12-10 15:32:52 -08:00
|
|
|
|
2020-01-04 00:45:06 -05:00
|
|
|
for (auto& display : displays) {
|
2023-12-10 12:32:44 -05:00
|
|
|
if (auto* layer = display.FindLayer(layer_id); layer) {
|
2024-01-17 22:03:40 -05:00
|
|
|
return layer->Close();
|
2023-12-10 12:32:44 -05:00
|
|
|
}
|
|
|
|
}
|
2024-01-17 22:03:40 -05:00
|
|
|
|
|
|
|
return false;
|
2023-12-10 12:32:44 -05:00
|
|
|
}
|
|
|
|
|
2024-01-06 21:21:01 -05:00
|
|
|
void Nvnflinger::SetLayerVisibility(u64 layer_id, bool visible) {
|
|
|
|
const auto lock_guard = Lock();
|
|
|
|
|
|
|
|
for (auto& display : displays) {
|
|
|
|
if (auto* layer = display.FindLayer(layer_id); layer) {
|
|
|
|
layer->SetVisibility(visible);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-10 12:32:44 -05:00
|
|
|
void Nvnflinger::DestroyLayer(u64 layer_id) {
|
|
|
|
const auto lock_guard = Lock();
|
|
|
|
|
|
|
|
for (auto& display : displays) {
|
|
|
|
display.DestroyLayer(layer_id);
|
2020-01-04 00:45:06 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
std::optional<u32> Nvnflinger::FindBufferQueueId(u64 display_id, u64 layer_id) {
|
2021-04-26 09:11:33 -04:00
|
|
|
const auto lock_guard = Lock();
|
2023-12-10 12:32:44 -05:00
|
|
|
const auto* const layer = FindLayer(display_id, layer_id);
|
2019-02-05 16:20:04 -05:00
|
|
|
|
|
|
|
if (layer == nullptr) {
|
2020-09-22 17:31:53 -04:00
|
|
|
return std::nullopt;
|
2019-02-05 16:20:04 -05:00
|
|
|
}
|
|
|
|
|
2021-11-11 19:15:51 -08:00
|
|
|
return layer->GetBinderId();
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-07-14 20:16:39 -04:00
|
|
|
Result Nvnflinger::FindVsyncEvent(Kernel::KReadableEvent** out_vsync_event, u64 display_id) {
|
2021-04-10 02:34:26 -07:00
|
|
|
const auto lock_guard = Lock();
|
2019-02-05 16:20:04 -05:00
|
|
|
auto* const display = FindDisplay(display_id);
|
|
|
|
|
|
|
|
if (display == nullptr) {
|
2022-09-25 21:20:36 -04:00
|
|
|
return VI::ResultNotFound;
|
2019-02-05 16:20:04 -05:00
|
|
|
}
|
|
|
|
|
2024-01-06 23:58:04 -05:00
|
|
|
*out_vsync_event = display->GetVSyncEvent();
|
|
|
|
return ResultSuccess;
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
VI::Display* Nvnflinger::FindDisplay(u64 display_id) {
|
2019-02-19 17:00:03 -05:00
|
|
|
const auto itr =
|
|
|
|
std::find_if(displays.begin(), displays.end(),
|
2019-02-21 10:43:26 -05:00
|
|
|
[&](const VI::Display& display) { return display.GetID() == display_id; });
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2019-02-05 16:20:04 -05:00
|
|
|
if (itr == displays.end()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return &*itr;
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
const VI::Display* Nvnflinger::FindDisplay(u64 display_id) const {
|
2019-02-19 17:00:03 -05:00
|
|
|
const auto itr =
|
|
|
|
std::find_if(displays.begin(), displays.end(),
|
2019-02-21 10:43:26 -05:00
|
|
|
[&](const VI::Display& display) { return display.GetID() == display_id; });
|
2019-01-30 11:14:05 -05:00
|
|
|
|
2019-02-05 16:20:04 -05:00
|
|
|
if (itr == displays.end()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return &*itr;
|
2019-01-30 11:14:05 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
VI::Layer* Nvnflinger::FindLayer(u64 display_id, u64 layer_id) {
|
2019-02-05 16:20:04 -05:00
|
|
|
auto* const display = FindDisplay(display_id);
|
2018-01-22 11:54:58 -05:00
|
|
|
|
2019-02-05 16:20:04 -05:00
|
|
|
if (display == nullptr) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2019-02-21 10:43:26 -05:00
|
|
|
return display->FindLayer(layer_id);
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
void Nvnflinger::Compose() {
|
2018-01-22 11:54:58 -05:00
|
|
|
for (auto& display : displays) {
|
|
|
|
// Trigger vsync for this display at the end of drawing
|
2019-02-21 10:43:26 -05:00
|
|
|
SCOPE_EXIT({ display.SignalVSyncEvent(); });
|
2018-01-22 11:54:58 -05:00
|
|
|
|
|
|
|
// Don't do anything for displays without layers.
|
2024-01-22 12:40:50 -05:00
|
|
|
if (!display.HasLayers()) {
|
2018-01-22 11:54:58 -05:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-10-28 17:17:38 -07:00
|
|
|
if (!system.IsPoweredOn()) {
|
|
|
|
return; // We are likely shutting down
|
|
|
|
}
|
|
|
|
|
2021-11-05 01:44:11 +01:00
|
|
|
auto nvdisp = nvdrv->GetDevice<Nvidia::Devices::nvdisp_disp0>(disp_fd);
|
2018-01-22 11:54:58 -05:00
|
|
|
ASSERT(nvdisp);
|
|
|
|
|
2024-02-03 17:14:43 -05:00
|
|
|
swap_interval = display.GetComposer().ComposeLocked(&compose_speed_scale, display, *nvdisp);
|
2018-01-22 11:54:58 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
s64 Nvnflinger::GetNextTicks() const {
|
2021-07-22 21:03:50 -04:00
|
|
|
const auto& settings = Settings::values;
|
2022-06-26 13:58:06 -07:00
|
|
|
auto speed_scale = 1.f;
|
2022-07-15 22:14:00 -07:00
|
|
|
if (settings.use_multi_core.GetValue()) {
|
|
|
|
if (settings.use_speed_limit.GetValue()) {
|
|
|
|
// Scales the speed based on speed_limit setting on MC. SC is handled by
|
|
|
|
// SpeedLimiter::DoSpeedLimiting.
|
|
|
|
speed_scale = 100.f / settings.speed_limit.GetValue();
|
|
|
|
} else {
|
|
|
|
// Run at unlocked framerate.
|
|
|
|
speed_scale = 0.01f;
|
|
|
|
}
|
2022-06-26 13:58:06 -07:00
|
|
|
}
|
2024-01-23 10:19:55 -05:00
|
|
|
|
|
|
|
// Adjust by speed limit determined during composition.
|
|
|
|
speed_scale /= compose_speed_scale;
|
|
|
|
|
2023-06-08 01:15:51 -04:00
|
|
|
if (system.GetNVDECActive() && settings.use_video_framerate.GetValue()) {
|
|
|
|
// Run at intended presentation rate during video playback.
|
|
|
|
speed_scale = 1.f;
|
|
|
|
}
|
2022-07-16 13:26:47 -07:00
|
|
|
|
2024-01-23 10:19:55 -05:00
|
|
|
const f32 effective_fps = 60.f / static_cast<f32>(swap_interval);
|
2023-01-11 21:57:30 -05:00
|
|
|
return static_cast<s64>(speed_scale * (1000000000.f / effective_fps));
|
2019-06-04 16:10:07 -04:00
|
|
|
}
|
|
|
|
|
2023-09-28 23:45:49 -06:00
|
|
|
FbShareBufferManager& Nvnflinger::GetSystemBufferManager() {
|
|
|
|
const auto lock_guard = Lock();
|
|
|
|
|
|
|
|
if (!system_buffer_manager) {
|
|
|
|
system_buffer_manager = std::make_unique<FbShareBufferManager>(system, *this, nvdrv);
|
|
|
|
}
|
|
|
|
|
|
|
|
return *system_buffer_manager;
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:05:34 -05:00
|
|
|
} // namespace Service::Nvnflinger
|