metal: split video object model
This commit is contained in:
parent
774177390e
commit
7a49da0a48
5 changed files with 424 additions and 354 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
mod copy_device;
|
mod copy_device;
|
||||||
mod hardware_cursor;
|
mod hardware_cursor;
|
||||||
mod lease;
|
mod lease;
|
||||||
|
mod model;
|
||||||
mod properties;
|
mod properties;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|
@ -8,6 +9,12 @@ pub use {
|
||||||
copy_device::CopyDeviceHolder,
|
copy_device::CopyDeviceHolder,
|
||||||
hardware_cursor::{MetalHardwareCursor, MetalHardwareCursorChange},
|
hardware_cursor::{MetalHardwareCursor, MetalHardwareCursorChange},
|
||||||
lease::{MetalLease, MetalLeaseData},
|
lease::{MetalLease, MetalLeaseData},
|
||||||
|
model::{
|
||||||
|
ConnectorDisplayData, ConnectorFutures, FrontState, HandleEvents, MetalConnector,
|
||||||
|
MetalCrtc, MetalDrmDevice, MetalDrmDeviceData, MetalEncoder, MetalLeaseId, MetalLeaseIds,
|
||||||
|
MetalPlane, MetalRenderContext, PendingDrmDevice, PersistentDisplayData, PlaneFormat,
|
||||||
|
PlaneType,
|
||||||
|
},
|
||||||
properties::{DefaultProperty, TypedProperty},
|
properties::{DefaultProperty, TypedProperty},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,13 +24,13 @@ use properties::{
|
||||||
|
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
async_engine::{Phase, SpawnedFuture},
|
async_engine::Phase,
|
||||||
backend::{
|
backend::{
|
||||||
BackendColorSpace, BackendConnectorState, BackendDrmDevice, BackendDrmLease,
|
BackendColorSpace, BackendConnectorState, BackendDrmDevice, BackendDrmLessee,
|
||||||
BackendDrmLessee, BackendEotfs, BackendEvent, BackendGammaLut, BackendGammaLutElement,
|
BackendEotfs, BackendEvent, BackendGammaLut, BackendGammaLutElement,
|
||||||
BackendLuminance, CONCAP_CONNECTOR, CONCAP_MODE_SETTING, CONCAP_PHYSICAL_DISPLAY,
|
BackendLuminance, CONCAP_CONNECTOR, CONCAP_MODE_SETTING, CONCAP_PHYSICAL_DISPLAY,
|
||||||
Connector, ConnectorCaps, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId,
|
Connector, ConnectorCaps, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId,
|
||||||
HardwareCursor, HardwareCursorUpdate, Mode, MonitorInfo, OutputId,
|
Mode, MonitorInfo, OutputId,
|
||||||
transaction::{
|
transaction::{
|
||||||
BackendConnectorTransaction, BackendConnectorTransactionError,
|
BackendConnectorTransaction, BackendConnectorTransactionError,
|
||||||
BackendConnectorTransactionType, BackendConnectorTransactionTypeDyn,
|
BackendConnectorTransactionType, BackendConnectorTransactionTypeDyn,
|
||||||
|
|
@ -31,37 +38,32 @@ use {
|
||||||
},
|
},
|
||||||
backends::metal::{
|
backends::metal::{
|
||||||
MetalBackend, MetalError,
|
MetalBackend, MetalError,
|
||||||
allocator::RenderBuffer,
|
|
||||||
present::{
|
present::{
|
||||||
DEFAULT_POST_COMMIT_MARGIN, DEFAULT_PRE_COMMIT_MARGIN, DirectScanoutCache,
|
DEFAULT_POST_COMMIT_MARGIN, DEFAULT_PRE_COMMIT_MARGIN, POST_COMMIT_MARGIN_DELTA,
|
||||||
POST_COMMIT_MARGIN_DELTA, PresentFb,
|
|
||||||
},
|
},
|
||||||
transaction::{DrmConnectorState, DrmCrtcState, DrmPlaneState, MetalDeviceTransaction},
|
transaction::{DrmConnectorState, DrmCrtcState, DrmPlaneState, MetalDeviceTransaction},
|
||||||
},
|
},
|
||||||
cmm::{cmm_description::ColorDescription, cmm_primaries::Primaries},
|
cmm::cmm_primaries::Primaries,
|
||||||
copy_device::{CopyDevice, CopyDeviceRegistry},
|
|
||||||
drm_feedback::DrmFeedback,
|
drm_feedback::DrmFeedback,
|
||||||
edid::{CtaDataBlock, Descriptor, EdidExtension},
|
edid::{CtaDataBlock, Descriptor, EdidExtension},
|
||||||
format::{Format, XRGB8888},
|
format::XRGB8888,
|
||||||
gfx_api::{FdSync, GfxApi, GfxContext, GfxFramebuffer},
|
gfx_api::GfxApi,
|
||||||
ifs::wp_presentation_feedback::{KIND_HW_COMPLETION, KIND_VSYNC, KIND_ZERO_COPY},
|
ifs::wp_presentation_feedback::{KIND_HW_COMPLETION, KIND_VSYNC, KIND_ZERO_COPY},
|
||||||
state::State,
|
|
||||||
tree::OutputNode,
|
tree::OutputNode,
|
||||||
udev::UdevDevice,
|
udev::UdevDevice,
|
||||||
utils::{
|
utils::{
|
||||||
asyncevent::AsyncEvent, binary_search_map::BinarySearchMap, bitflags::BitflagsExt,
|
binary_search_map::BinarySearchMap, bitflags::BitflagsExt, cell_ext::CellExt,
|
||||||
cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt,
|
clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt,
|
||||||
geometric_decay::GeometricDecay, numcell::NumCell, on_change::OnChange,
|
geometric_decay::GeometricDecay, numcell::NumCell, ordered_float::F64,
|
||||||
opaque_cell::OpaqueCell, ordered_float::F64, oserror::OsError,
|
oserror::OsError,
|
||||||
},
|
},
|
||||||
video::{
|
video::{
|
||||||
INVALID_MODIFIER, Modifier,
|
INVALID_MODIFIER,
|
||||||
dmabuf::DmaBufId,
|
|
||||||
drm::{
|
drm::{
|
||||||
ConnectorStatus, ConnectorType, DRM_CLIENT_CAP_ATOMIC, DrmBlob, DrmCardResources,
|
ConnectorStatus, ConnectorType, DRM_CLIENT_CAP_ATOMIC, DrmBlob, DrmCardResources,
|
||||||
DrmConnector, DrmCrtc, DrmEncoder, DrmError, DrmEvent, DrmFb, DrmLease, DrmMaster,
|
DrmConnector, DrmCrtc, DrmEncoder, DrmError, DrmEvent, DrmFb, DrmMaster,
|
||||||
DrmModeInfo, DrmObject, DrmPlane, DrmProperty, DrmPropertyDefinition,
|
DrmObject, DrmPlane, DrmProperty, DrmPropertyDefinition, DrmPropertyType,
|
||||||
DrmPropertyType, DrmVersion, HDMI_EOTF_TRADITIONAL_GAMMA_SDR, drm_mode_modeinfo,
|
DrmVersion, HDMI_EOTF_TRADITIONAL_GAMMA_SDR, drm_mode_modeinfo,
|
||||||
hdr_output_metadata,
|
hdr_output_metadata,
|
||||||
},
|
},
|
||||||
gbm::GbmDevice,
|
gbm::GbmDevice,
|
||||||
|
|
@ -69,82 +71,18 @@ use {
|
||||||
},
|
},
|
||||||
ahash::{AHashMap, AHashSet},
|
ahash::{AHashMap, AHashSet},
|
||||||
bstr::{BString, ByteSlice},
|
bstr::{BString, ByteSlice},
|
||||||
indexmap::{IndexSet, indexset},
|
indexmap::indexset,
|
||||||
isnt::std_1::collections::IsntHashMapExt,
|
isnt::std_1::collections::IsntHashMapExt,
|
||||||
std::{
|
std::{
|
||||||
cell::{Cell, OnceCell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::hash_map::Entry,
|
collections::hash_map::Entry,
|
||||||
ffi::CString,
|
|
||||||
fmt::{Debug, Formatter},
|
|
||||||
mem,
|
mem,
|
||||||
ops::DerefMut,
|
ops::DerefMut,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
},
|
},
|
||||||
uapi::{
|
uapi::c::{self, dev_t},
|
||||||
OwnedFd,
|
|
||||||
c::{self, dev_t},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PendingDrmDevice {
|
|
||||||
pub id: DrmDeviceId,
|
|
||||||
pub devnum: c::dev_t,
|
|
||||||
pub devnode: CString,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MetalRenderContext {
|
|
||||||
pub dev_id: DrmDeviceId,
|
|
||||||
pub gfx: Rc<dyn GfxContext>,
|
|
||||||
pub gbm: Rc<GbmDevice>,
|
|
||||||
pub devnode: CString,
|
|
||||||
pub copy_device: Rc<CopyDeviceHolder>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct MetalDrmDevice {
|
|
||||||
pub backend: Rc<MetalBackend>,
|
|
||||||
pub id: DrmDeviceId,
|
|
||||||
pub devnum: c::dev_t,
|
|
||||||
pub devnode: CString,
|
|
||||||
pub master: Rc<DrmMaster>,
|
|
||||||
pub supports_kms: bool,
|
|
||||||
pub crtcs: AHashMap<DrmCrtc, Rc<MetalCrtc>>,
|
|
||||||
pub encoders: AHashMap<DrmEncoder, Rc<MetalEncoder>>,
|
|
||||||
pub planes: AHashMap<DrmPlane, Rc<MetalPlane>>,
|
|
||||||
pub cursor_width: u64,
|
|
||||||
pub cursor_height: u64,
|
|
||||||
pub supports_async_commit: bool,
|
|
||||||
pub gbm: Rc<GbmDevice>,
|
|
||||||
pub handle_events: HandleEvents,
|
|
||||||
pub ctx: CloneCell<Rc<MetalRenderContext>>,
|
|
||||||
pub copy_device: Rc<CopyDeviceHolder>,
|
|
||||||
pub on_change: OnChange<crate::backend::DrmEvent>,
|
|
||||||
pub direct_scanout_enabled: Cell<Option<bool>>,
|
|
||||||
pub is_nvidia: bool,
|
|
||||||
pub _is_amd: bool,
|
|
||||||
pub lease_ids: MetalLeaseIds,
|
|
||||||
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
|
||||||
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
|
||||||
pub paused: Cell<bool>,
|
|
||||||
pub min_post_commit_margin: Cell<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MetalDrmDevice {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("MetalDrmDevice").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MetalDrmDevice {
|
|
||||||
pub fn is_render_device(&self) -> bool {
|
|
||||||
if let Some(ctx) = self.backend.ctx.get() {
|
|
||||||
return ctx.dev_id == self.id;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackendDrmDevice for MetalDrmDevice {
|
impl BackendDrmDevice for MetalDrmDevice {
|
||||||
fn id(&self) -> DrmDeviceId {
|
fn id(&self) -> DrmDeviceId {
|
||||||
self.id
|
self.id
|
||||||
|
|
@ -326,180 +264,6 @@ impl BackendDrmDevice for MetalDrmDevice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HandleEvents {
|
|
||||||
pub handle_events: Cell<Option<SpawnedFuture<()>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for HandleEvents {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("HandleEvents").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MetalDrmDeviceData {
|
|
||||||
pub dev: Rc<MetalDrmDevice>,
|
|
||||||
pub connectors: CopyHashMap<DrmConnector, Rc<MetalConnector>>,
|
|
||||||
pub futures: CopyHashMap<DrmConnector, ConnectorFutures>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PersistentDisplayData {
|
|
||||||
pub state: RefCell<BackendConnectorState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ConnectorDisplayData {
|
|
||||||
pub crtc_id: DrmProperty,
|
|
||||||
pub crtcs: BinarySearchMap<DrmCrtc, Rc<MetalCrtc>, 8>,
|
|
||||||
pub first_mode: Mode,
|
|
||||||
pub modes: Vec<DrmModeInfo>,
|
|
||||||
pub persistent: Rc<PersistentDisplayData>,
|
|
||||||
pub refresh: u32,
|
|
||||||
pub non_desktop: bool,
|
|
||||||
pub non_desktop_effective: bool,
|
|
||||||
pub vrr_capable: bool,
|
|
||||||
pub _vrr_refresh_max_nsec: u64,
|
|
||||||
pub default_properties: Vec<DefaultProperty>,
|
|
||||||
pub untyped_properties: AHashMap<DrmProperty, u64>,
|
|
||||||
|
|
||||||
pub connector_id: ConnectorKernelId,
|
|
||||||
pub output_id: Rc<OutputId>,
|
|
||||||
|
|
||||||
pub connection: ConnectorStatus,
|
|
||||||
pub mm_width: u32,
|
|
||||||
pub mm_height: u32,
|
|
||||||
pub _subpixel: u32,
|
|
||||||
|
|
||||||
pub supports_bt2020: bool,
|
|
||||||
pub supports_pq: bool,
|
|
||||||
pub primaries: Primaries,
|
|
||||||
pub luminance: Option<BackendLuminance>,
|
|
||||||
|
|
||||||
pub colorspace: Option<DrmProperty>,
|
|
||||||
pub hdr_metadata: Option<DrmProperty>,
|
|
||||||
pub drm_state: DrmConnectorState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectorDisplayData {
|
|
||||||
fn update_refresh(&mut self, dev: &MetalDrmDevice) {
|
|
||||||
self.refresh = 0;
|
|
||||||
if self.drm_state.crtc_id.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(crtc) = dev.crtcs.get(&self.drm_state.crtc_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let drm_state = &*crtc.drm_state.borrow();
|
|
||||||
let Some(mode) = &drm_state.mode else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let refresh_rate_mhz = mode.refresh_rate_millihz();
|
|
||||||
if refresh_rate_mhz != 0 {
|
|
||||||
self.refresh = (1_000_000_000_000u64 / refresh_rate_mhz as u64) as u32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_non_desktop_effective(&mut self) {
|
|
||||||
let state = &*self.persistent.state.borrow();
|
|
||||||
self.non_desktop_effective =
|
|
||||||
!state.enabled || state.non_desktop_override.unwrap_or(self.non_desktop);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_cached_fields(&mut self, dev: &MetalDrmDevice) {
|
|
||||||
self.update_refresh(dev);
|
|
||||||
self.update_non_desktop_effective();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_ids!(MetalLeaseIds, MetalLeaseId, u64);
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum FrontState {
|
|
||||||
Removed,
|
|
||||||
Disconnected,
|
|
||||||
Connected { non_desktop: bool },
|
|
||||||
Unavailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MetalConnector {
|
|
||||||
pub id: DrmConnector,
|
|
||||||
pub kernel_id: Cell<ConnectorKernelId>,
|
|
||||||
pub master: Rc<DrmMaster>,
|
|
||||||
pub state: Rc<State>,
|
|
||||||
|
|
||||||
pub dev: Rc<MetalDrmDevice>,
|
|
||||||
pub backend: Rc<MetalBackend>,
|
|
||||||
|
|
||||||
pub connector_id: ConnectorId,
|
|
||||||
|
|
||||||
pub buffers: CloneCell<Option<Rc<[RenderBuffer; 2]>>>,
|
|
||||||
pub color_description: CloneCell<Rc<ColorDescription>>,
|
|
||||||
|
|
||||||
pub lease: Cell<Option<MetalLeaseId>>,
|
|
||||||
|
|
||||||
pub buffers_idle: Cell<bool>,
|
|
||||||
pub crtc_idle: Cell<bool>,
|
|
||||||
pub has_damage: NumCell<u64>,
|
|
||||||
pub cursor_changed: Cell<bool>,
|
|
||||||
pub cursor_damage: Cell<bool>,
|
|
||||||
pub next_vblank_nsec: Cell<u64>,
|
|
||||||
|
|
||||||
pub display: RefCell<ConnectorDisplayData>,
|
|
||||||
|
|
||||||
pub frontend_state: Cell<FrontState>,
|
|
||||||
|
|
||||||
pub primary_plane: CloneCell<Option<Rc<MetalPlane>>>,
|
|
||||||
pub cursor_plane: CloneCell<Option<Rc<MetalPlane>>>,
|
|
||||||
|
|
||||||
pub crtc: CloneCell<Option<Rc<MetalCrtc>>>,
|
|
||||||
|
|
||||||
pub on_change: OnChange<ConnectorEvent>,
|
|
||||||
|
|
||||||
pub present_trigger: AsyncEvent,
|
|
||||||
|
|
||||||
pub cursor_x: Cell<i32>,
|
|
||||||
pub cursor_y: Cell<i32>,
|
|
||||||
pub cursor_enabled: Cell<bool>,
|
|
||||||
pub cursor_buffers: CloneCell<Option<Rc<[RenderBuffer; 2]>>>,
|
|
||||||
pub cursor_swap_buffer: Cell<bool>,
|
|
||||||
pub cursor_sync: CloneCell<Option<FdSync>>,
|
|
||||||
|
|
||||||
pub drm_feedback: CloneCell<Option<Rc<DrmFeedback>>>,
|
|
||||||
pub scanout_buffers: RefCell<AHashMap<DmaBufId, DirectScanoutCache>>,
|
|
||||||
pub active_framebuffer: RefCell<Option<PresentFb>>,
|
|
||||||
pub next_framebuffer: OpaqueCell<Option<PresentFb>>,
|
|
||||||
pub direct_scanout_active: Cell<bool>,
|
|
||||||
|
|
||||||
pub version: NumCell<u64>,
|
|
||||||
pub expected_sequence: Cell<Option<u64>>,
|
|
||||||
pub pre_commit_margin: Cell<u64>,
|
|
||||||
pub pre_commit_margin_decay: GeometricDecay,
|
|
||||||
pub post_commit_margin: Cell<u64>,
|
|
||||||
pub post_commit_margin_decay: GeometricDecay,
|
|
||||||
pub vblank_miss_sec: Cell<u32>,
|
|
||||||
pub vblank_miss_this_sec: NumCell<u32>,
|
|
||||||
pub presentation_is_sync: Cell<bool>,
|
|
||||||
pub presentation_is_zero_copy: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MetalConnector {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("MetalConnnector").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectorFutures {
|
|
||||||
pub _present: SpawnedFuture<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for ConnectorFutures {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("ConnectorFutures").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MetalConnector {
|
impl MetalConnector {
|
||||||
pub fn send_connected(self: &Rc<Self>) {
|
pub fn send_connected(self: &Rc<Self>) {
|
||||||
let dd = &*self.display.borrow();
|
let dd = &*self.display.borrow();
|
||||||
|
|
@ -792,95 +556,6 @@ impl Connector for MetalConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MetalCrtc {
|
|
||||||
pub id: DrmCrtc,
|
|
||||||
pub idx: usize,
|
|
||||||
pub master: Rc<DrmMaster>,
|
|
||||||
pub default_properties: Vec<DefaultProperty>,
|
|
||||||
pub untyped_properties: RefCell<AHashMap<DrmProperty, u64>>,
|
|
||||||
|
|
||||||
pub lease: Cell<Option<MetalLeaseId>>,
|
|
||||||
|
|
||||||
pub possible_planes: BinarySearchMap<DrmPlane, Rc<MetalPlane>, 8>,
|
|
||||||
|
|
||||||
pub connector: CloneCell<Option<Rc<MetalConnector>>>,
|
|
||||||
pub pending_flip: CloneCell<Option<Rc<MetalConnector>>>,
|
|
||||||
|
|
||||||
pub active: DrmProperty,
|
|
||||||
pub mode_id: DrmProperty,
|
|
||||||
pub vrr_enabled: DrmProperty,
|
|
||||||
pub out_fence_ptr: DrmProperty,
|
|
||||||
pub gamma_lut: Option<DrmProperty>,
|
|
||||||
pub gamma_lut_size: Option<u32>,
|
|
||||||
pub drm_state: RefCell<DrmCrtcState>,
|
|
||||||
|
|
||||||
pub sequence: Cell<u64>,
|
|
||||||
pub have_queued_sequence: Cell<bool>,
|
|
||||||
pub needs_vblank_emulation: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MetalCrtc {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("MetalCrtc").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MetalEncoder {
|
|
||||||
pub id: DrmEncoder,
|
|
||||||
pub crtcs: AHashMap<DrmCrtc, Rc<MetalCrtc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
pub enum PlaneType {
|
|
||||||
Overlay,
|
|
||||||
Primary,
|
|
||||||
Cursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PlaneFormat {
|
|
||||||
pub format: &'static Format,
|
|
||||||
pub modifiers: IndexSet<Modifier>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MetalPlane {
|
|
||||||
pub id: DrmPlane,
|
|
||||||
pub master: Rc<DrmMaster>,
|
|
||||||
pub default_properties: Vec<DefaultProperty>,
|
|
||||||
pub untyped_properties: RefCell<AHashMap<DrmProperty, u64>>,
|
|
||||||
|
|
||||||
pub ty: PlaneType,
|
|
||||||
|
|
||||||
pub possible_crtcs: u32,
|
|
||||||
pub formats: AHashMap<u32, PlaneFormat>,
|
|
||||||
|
|
||||||
pub lease: Cell<Option<MetalLeaseId>>,
|
|
||||||
|
|
||||||
pub mode_w: Cell<i32>,
|
|
||||||
pub mode_h: Cell<i32>,
|
|
||||||
|
|
||||||
pub crtc_id: DrmProperty,
|
|
||||||
pub crtc_x: DrmProperty,
|
|
||||||
pub crtc_y: DrmProperty,
|
|
||||||
pub crtc_w: DrmProperty,
|
|
||||||
pub crtc_h: DrmProperty,
|
|
||||||
pub src_x: DrmProperty,
|
|
||||||
pub src_y: DrmProperty,
|
|
||||||
pub src_w: DrmProperty,
|
|
||||||
pub src_h: DrmProperty,
|
|
||||||
pub in_fence_fd: DrmProperty,
|
|
||||||
pub fb_id: DrmProperty,
|
|
||||||
|
|
||||||
pub drm_state: RefCell<DrmPlaneState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MetalPlane {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("MetalPlane").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_connectors(
|
fn get_connectors(
|
||||||
backend: &Rc<MetalBackend>,
|
backend: &Rc<MetalBackend>,
|
||||||
dev: &Rc<MetalDrmDevice>,
|
dev: &Rc<MetalDrmDevice>,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
use super::*;
|
use {
|
||||||
|
crate::{
|
||||||
|
copy_device::{CopyDevice, CopyDeviceRegistry},
|
||||||
|
utils::errorfmt::ErrorFmt,
|
||||||
|
},
|
||||||
|
std::{
|
||||||
|
cell::OnceCell,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
rc::Rc,
|
||||||
|
},
|
||||||
|
uapi::c::dev_t,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct CopyDeviceHolder {
|
pub struct CopyDeviceHolder {
|
||||||
pub registry: Rc<CopyDeviceRegistry>,
|
pub registry: Rc<CopyDeviceRegistry>,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
use super::*;
|
use {
|
||||||
|
super::MetalConnector,
|
||||||
|
crate::{
|
||||||
|
backend::{HardwareCursor, HardwareCursorUpdate},
|
||||||
|
backends::metal::allocator::RenderBuffer,
|
||||||
|
gfx_api::{FdSync, GfxFramebuffer},
|
||||||
|
},
|
||||||
|
std::{
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
rc::Rc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct MetalHardwareCursor {
|
pub struct MetalHardwareCursor {
|
||||||
pub connector: Rc<MetalConnector>,
|
pub connector: Rc<MetalConnector>,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
use super::*;
|
use {
|
||||||
|
super::{FrontState, MetalConnector, MetalCrtc, MetalDrmDevice, MetalLeaseId, MetalPlane},
|
||||||
|
crate::{
|
||||||
|
backend::{BackendDrmLease, BackendDrmLessee, ConnectorEvent},
|
||||||
|
utils::errorfmt::ErrorFmt,
|
||||||
|
video::drm::DrmLease,
|
||||||
|
},
|
||||||
|
std::{cell::Cell, rc::Rc},
|
||||||
|
uapi::OwnedFd,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct MetalLeaseData {
|
pub struct MetalLeaseData {
|
||||||
pub lease: DrmLease,
|
pub lease: DrmLease,
|
||||||
|
|
|
||||||
364
src/backends/metal/video/model.rs
Normal file
364
src/backends/metal/video/model.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
use {
|
||||||
|
super::{copy_device::CopyDeviceHolder, lease::MetalLeaseData, properties::DefaultProperty},
|
||||||
|
crate::{
|
||||||
|
async_engine::SpawnedFuture,
|
||||||
|
backend::{
|
||||||
|
BackendConnectorState, BackendLuminance, ConnectorEvent, ConnectorId,
|
||||||
|
ConnectorKernelId, DrmDeviceId, DrmEvent, Mode, OutputId,
|
||||||
|
},
|
||||||
|
backends::metal::{
|
||||||
|
MetalBackend,
|
||||||
|
allocator::RenderBuffer,
|
||||||
|
present::{DirectScanoutCache, PresentFb},
|
||||||
|
transaction::{DrmConnectorState, DrmCrtcState, DrmPlaneState},
|
||||||
|
},
|
||||||
|
cmm::{cmm_description::ColorDescription, cmm_primaries::Primaries},
|
||||||
|
drm_feedback::DrmFeedback,
|
||||||
|
format::Format,
|
||||||
|
gfx_api::{FdSync, GfxContext},
|
||||||
|
state::State,
|
||||||
|
utils::{
|
||||||
|
asyncevent::AsyncEvent, binary_search_map::BinarySearchMap, clonecell::CloneCell,
|
||||||
|
copyhashmap::CopyHashMap, geometric_decay::GeometricDecay, numcell::NumCell,
|
||||||
|
on_change::OnChange, opaque_cell::OpaqueCell,
|
||||||
|
},
|
||||||
|
video::{
|
||||||
|
Modifier,
|
||||||
|
dmabuf::DmaBufId,
|
||||||
|
drm::{
|
||||||
|
ConnectorStatus, DrmConnector, DrmCrtc, DrmEncoder, DrmMaster, DrmModeInfo,
|
||||||
|
DrmObject, DrmPlane, DrmProperty,
|
||||||
|
},
|
||||||
|
gbm::GbmDevice,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ahash::AHashMap,
|
||||||
|
indexmap::IndexSet,
|
||||||
|
std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
ffi::CString,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
rc::Rc,
|
||||||
|
},
|
||||||
|
uapi::c,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PendingDrmDevice {
|
||||||
|
pub id: DrmDeviceId,
|
||||||
|
pub devnum: c::dev_t,
|
||||||
|
pub devnode: CString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MetalRenderContext {
|
||||||
|
pub dev_id: DrmDeviceId,
|
||||||
|
pub gfx: Rc<dyn GfxContext>,
|
||||||
|
pub gbm: Rc<GbmDevice>,
|
||||||
|
pub devnode: CString,
|
||||||
|
pub copy_device: Rc<CopyDeviceHolder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetalDrmDevice {
|
||||||
|
pub backend: Rc<MetalBackend>,
|
||||||
|
pub id: DrmDeviceId,
|
||||||
|
pub devnum: c::dev_t,
|
||||||
|
pub devnode: CString,
|
||||||
|
pub master: Rc<DrmMaster>,
|
||||||
|
pub supports_kms: bool,
|
||||||
|
pub crtcs: AHashMap<DrmCrtc, Rc<MetalCrtc>>,
|
||||||
|
pub encoders: AHashMap<DrmEncoder, Rc<MetalEncoder>>,
|
||||||
|
pub planes: AHashMap<DrmPlane, Rc<MetalPlane>>,
|
||||||
|
pub cursor_width: u64,
|
||||||
|
pub cursor_height: u64,
|
||||||
|
pub supports_async_commit: bool,
|
||||||
|
pub gbm: Rc<GbmDevice>,
|
||||||
|
pub handle_events: HandleEvents,
|
||||||
|
pub ctx: CloneCell<Rc<MetalRenderContext>>,
|
||||||
|
pub copy_device: Rc<CopyDeviceHolder>,
|
||||||
|
pub on_change: OnChange<DrmEvent>,
|
||||||
|
pub direct_scanout_enabled: Cell<Option<bool>>,
|
||||||
|
pub is_nvidia: bool,
|
||||||
|
pub _is_amd: bool,
|
||||||
|
pub lease_ids: MetalLeaseIds,
|
||||||
|
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||||
|
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||||
|
pub paused: Cell<bool>,
|
||||||
|
pub min_post_commit_margin: Cell<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MetalDrmDevice {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("MetalDrmDevice").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetalDrmDevice {
|
||||||
|
pub fn is_render_device(&self) -> bool {
|
||||||
|
if let Some(ctx) = self.backend.ctx.get() {
|
||||||
|
return ctx.dev_id == self.id;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HandleEvents {
|
||||||
|
pub handle_events: Cell<Option<SpawnedFuture<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for HandleEvents {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("HandleEvents").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MetalDrmDeviceData {
|
||||||
|
pub dev: Rc<MetalDrmDevice>,
|
||||||
|
pub connectors: CopyHashMap<DrmConnector, Rc<MetalConnector>>,
|
||||||
|
pub futures: CopyHashMap<DrmConnector, ConnectorFutures>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PersistentDisplayData {
|
||||||
|
pub state: RefCell<BackendConnectorState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConnectorDisplayData {
|
||||||
|
pub crtc_id: DrmProperty,
|
||||||
|
pub crtcs: BinarySearchMap<DrmCrtc, Rc<MetalCrtc>, 8>,
|
||||||
|
pub first_mode: Mode,
|
||||||
|
pub modes: Vec<DrmModeInfo>,
|
||||||
|
pub persistent: Rc<PersistentDisplayData>,
|
||||||
|
pub refresh: u32,
|
||||||
|
pub non_desktop: bool,
|
||||||
|
pub non_desktop_effective: bool,
|
||||||
|
pub vrr_capable: bool,
|
||||||
|
pub _vrr_refresh_max_nsec: u64,
|
||||||
|
pub default_properties: Vec<DefaultProperty>,
|
||||||
|
pub untyped_properties: AHashMap<DrmProperty, u64>,
|
||||||
|
|
||||||
|
pub connector_id: ConnectorKernelId,
|
||||||
|
pub output_id: Rc<OutputId>,
|
||||||
|
|
||||||
|
pub connection: ConnectorStatus,
|
||||||
|
pub mm_width: u32,
|
||||||
|
pub mm_height: u32,
|
||||||
|
pub _subpixel: u32,
|
||||||
|
|
||||||
|
pub supports_bt2020: bool,
|
||||||
|
pub supports_pq: bool,
|
||||||
|
pub primaries: Primaries,
|
||||||
|
pub luminance: Option<BackendLuminance>,
|
||||||
|
|
||||||
|
pub colorspace: Option<DrmProperty>,
|
||||||
|
pub hdr_metadata: Option<DrmProperty>,
|
||||||
|
pub drm_state: DrmConnectorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectorDisplayData {
|
||||||
|
fn update_refresh(&mut self, dev: &MetalDrmDevice) {
|
||||||
|
self.refresh = 0;
|
||||||
|
if self.drm_state.crtc_id.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(crtc) = dev.crtcs.get(&self.drm_state.crtc_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let drm_state = &*crtc.drm_state.borrow();
|
||||||
|
let Some(mode) = &drm_state.mode else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let refresh_rate_mhz = mode.refresh_rate_millihz();
|
||||||
|
if refresh_rate_mhz != 0 {
|
||||||
|
self.refresh = (1_000_000_000_000u64 / refresh_rate_mhz as u64) as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_non_desktop_effective(&mut self) {
|
||||||
|
let state = &*self.persistent.state.borrow();
|
||||||
|
self.non_desktop_effective =
|
||||||
|
!state.enabled || state.non_desktop_override.unwrap_or(self.non_desktop);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_cached_fields(&mut self, dev: &MetalDrmDevice) {
|
||||||
|
self.update_refresh(dev);
|
||||||
|
self.update_non_desktop_effective();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
linear_ids!(MetalLeaseIds, MetalLeaseId, u64);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FrontState {
|
||||||
|
Removed,
|
||||||
|
Disconnected,
|
||||||
|
Connected { non_desktop: bool },
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetalConnector {
|
||||||
|
pub id: DrmConnector,
|
||||||
|
pub kernel_id: Cell<ConnectorKernelId>,
|
||||||
|
pub master: Rc<DrmMaster>,
|
||||||
|
pub state: Rc<State>,
|
||||||
|
|
||||||
|
pub dev: Rc<MetalDrmDevice>,
|
||||||
|
pub backend: Rc<MetalBackend>,
|
||||||
|
|
||||||
|
pub connector_id: ConnectorId,
|
||||||
|
|
||||||
|
pub buffers: CloneCell<Option<Rc<[RenderBuffer; 2]>>>,
|
||||||
|
pub color_description: CloneCell<Rc<ColorDescription>>,
|
||||||
|
|
||||||
|
pub lease: Cell<Option<MetalLeaseId>>,
|
||||||
|
|
||||||
|
pub buffers_idle: Cell<bool>,
|
||||||
|
pub crtc_idle: Cell<bool>,
|
||||||
|
pub has_damage: NumCell<u64>,
|
||||||
|
pub cursor_changed: Cell<bool>,
|
||||||
|
pub cursor_damage: Cell<bool>,
|
||||||
|
pub next_vblank_nsec: Cell<u64>,
|
||||||
|
|
||||||
|
pub display: RefCell<ConnectorDisplayData>,
|
||||||
|
|
||||||
|
pub frontend_state: Cell<FrontState>,
|
||||||
|
|
||||||
|
pub primary_plane: CloneCell<Option<Rc<MetalPlane>>>,
|
||||||
|
pub cursor_plane: CloneCell<Option<Rc<MetalPlane>>>,
|
||||||
|
|
||||||
|
pub crtc: CloneCell<Option<Rc<MetalCrtc>>>,
|
||||||
|
|
||||||
|
pub on_change: OnChange<ConnectorEvent>,
|
||||||
|
|
||||||
|
pub present_trigger: AsyncEvent,
|
||||||
|
|
||||||
|
pub cursor_x: Cell<i32>,
|
||||||
|
pub cursor_y: Cell<i32>,
|
||||||
|
pub cursor_enabled: Cell<bool>,
|
||||||
|
pub cursor_buffers: CloneCell<Option<Rc<[RenderBuffer; 2]>>>,
|
||||||
|
pub cursor_swap_buffer: Cell<bool>,
|
||||||
|
pub cursor_sync: CloneCell<Option<FdSync>>,
|
||||||
|
|
||||||
|
pub drm_feedback: CloneCell<Option<Rc<DrmFeedback>>>,
|
||||||
|
pub scanout_buffers: RefCell<AHashMap<DmaBufId, DirectScanoutCache>>,
|
||||||
|
pub active_framebuffer: RefCell<Option<PresentFb>>,
|
||||||
|
pub next_framebuffer: OpaqueCell<Option<PresentFb>>,
|
||||||
|
pub direct_scanout_active: Cell<bool>,
|
||||||
|
|
||||||
|
pub version: NumCell<u64>,
|
||||||
|
pub expected_sequence: Cell<Option<u64>>,
|
||||||
|
pub pre_commit_margin: Cell<u64>,
|
||||||
|
pub pre_commit_margin_decay: GeometricDecay,
|
||||||
|
pub post_commit_margin: Cell<u64>,
|
||||||
|
pub post_commit_margin_decay: GeometricDecay,
|
||||||
|
pub vblank_miss_sec: Cell<u32>,
|
||||||
|
pub vblank_miss_this_sec: NumCell<u32>,
|
||||||
|
pub presentation_is_sync: Cell<bool>,
|
||||||
|
pub presentation_is_zero_copy: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MetalConnector {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("MetalConnnector").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConnectorFutures {
|
||||||
|
pub _present: SpawnedFuture<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for ConnectorFutures {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ConnectorFutures").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetalCrtc {
|
||||||
|
pub id: DrmCrtc,
|
||||||
|
pub idx: usize,
|
||||||
|
pub master: Rc<DrmMaster>,
|
||||||
|
pub default_properties: Vec<DefaultProperty>,
|
||||||
|
pub untyped_properties: RefCell<AHashMap<DrmProperty, u64>>,
|
||||||
|
|
||||||
|
pub lease: Cell<Option<MetalLeaseId>>,
|
||||||
|
|
||||||
|
pub possible_planes: BinarySearchMap<DrmPlane, Rc<MetalPlane>, 8>,
|
||||||
|
|
||||||
|
pub connector: CloneCell<Option<Rc<MetalConnector>>>,
|
||||||
|
pub pending_flip: CloneCell<Option<Rc<MetalConnector>>>,
|
||||||
|
|
||||||
|
pub active: DrmProperty,
|
||||||
|
pub mode_id: DrmProperty,
|
||||||
|
pub vrr_enabled: DrmProperty,
|
||||||
|
pub out_fence_ptr: DrmProperty,
|
||||||
|
pub gamma_lut: Option<DrmProperty>,
|
||||||
|
pub gamma_lut_size: Option<u32>,
|
||||||
|
pub drm_state: RefCell<DrmCrtcState>,
|
||||||
|
|
||||||
|
pub sequence: Cell<u64>,
|
||||||
|
pub have_queued_sequence: Cell<bool>,
|
||||||
|
pub needs_vblank_emulation: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MetalCrtc {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("MetalCrtc").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MetalEncoder {
|
||||||
|
pub id: DrmEncoder,
|
||||||
|
pub crtcs: AHashMap<DrmCrtc, Rc<MetalCrtc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum PlaneType {
|
||||||
|
Overlay,
|
||||||
|
Primary,
|
||||||
|
Cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlaneFormat {
|
||||||
|
pub format: &'static Format,
|
||||||
|
pub modifiers: IndexSet<Modifier>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetalPlane {
|
||||||
|
pub id: DrmPlane,
|
||||||
|
pub master: Rc<DrmMaster>,
|
||||||
|
pub default_properties: Vec<DefaultProperty>,
|
||||||
|
pub untyped_properties: RefCell<AHashMap<DrmProperty, u64>>,
|
||||||
|
|
||||||
|
pub ty: PlaneType,
|
||||||
|
|
||||||
|
pub possible_crtcs: u32,
|
||||||
|
pub formats: AHashMap<u32, PlaneFormat>,
|
||||||
|
|
||||||
|
pub lease: Cell<Option<MetalLeaseId>>,
|
||||||
|
|
||||||
|
pub mode_w: Cell<i32>,
|
||||||
|
pub mode_h: Cell<i32>,
|
||||||
|
|
||||||
|
pub crtc_id: DrmProperty,
|
||||||
|
pub crtc_x: DrmProperty,
|
||||||
|
pub crtc_y: DrmProperty,
|
||||||
|
pub crtc_w: DrmProperty,
|
||||||
|
pub crtc_h: DrmProperty,
|
||||||
|
pub src_x: DrmProperty,
|
||||||
|
pub src_y: DrmProperty,
|
||||||
|
pub src_w: DrmProperty,
|
||||||
|
pub src_h: DrmProperty,
|
||||||
|
pub in_fence_fd: DrmProperty,
|
||||||
|
pub fb_id: DrmProperty,
|
||||||
|
|
||||||
|
pub drm_state: RefCell<DrmPlaneState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MetalPlane {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("MetalPlane").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue