mirror of
https://github.com/darlinghq/darling-metal.git
synced 2024-11-26 22:20:22 +00:00
Implement MTKView
This commit is contained in:
parent
2db38fac36
commit
54b7f94d18
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
.vscode
|
.vscode
|
||||||
tmp
|
tmp
|
||||||
|
build
|
||||||
|
@ -99,7 +99,7 @@ add_framework(MetalKit
|
|||||||
VERSION ${FRAMEWORK_VERSION}
|
VERSION ${FRAMEWORK_VERSION}
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
src/dummy.c
|
src/MetalKit/MTKView.m
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
AppKit
|
AppKit
|
||||||
@ -107,12 +107,18 @@ add_framework(MetalKit
|
|||||||
objc
|
objc
|
||||||
Foundation
|
Foundation
|
||||||
Metal
|
Metal
|
||||||
|
QuartzCore
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MetalKit PUBLIC
|
target_include_directories(MetalKit PUBLIC
|
||||||
include
|
include
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(MetalKit PRIVATE
|
||||||
|
private-include
|
||||||
|
${CMAKE_SOURCE_DIR}/src/external/foundation/internal_include
|
||||||
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
# MetalPerformanceShaders
|
# MetalPerformanceShaders
|
||||||
#
|
#
|
||||||
|
79
include/MetalKit/MTKView.h
Normal file
79
include/MetalKit/MTKView.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Darling.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 Darling developers
|
||||||
|
*
|
||||||
|
* Darling is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Darling is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _METALKIT_MTKVIEW_H_
|
||||||
|
#define _METALKIT_MTKVIEW_H_
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <Metal/Metal.h>
|
||||||
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
|
||||||
|
@class MTKView;
|
||||||
|
|
||||||
|
@protocol MTKViewDelegate
|
||||||
|
|
||||||
|
- (void) mtkView: (MTKView*)view
|
||||||
|
drawableSizeWillChange: (CGSize)size;
|
||||||
|
|
||||||
|
- (void)drawInMTKView: (MTKView*)view;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
MTL_EXPORT
|
||||||
|
@interface MTKView : NSView <NSCoding, CALayerDelegate>
|
||||||
|
|
||||||
|
#if __OBJC2__
|
||||||
|
@property(nonatomic, weak, nullable) id<MTKViewDelegate> delegate;
|
||||||
|
#else
|
||||||
|
@property(nonatomic, assign, nullable) id<MTKViewDelegate> delegate;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@property(nonatomic, retain, nullable) id<MTLDevice> device;
|
||||||
|
@property(readonly) id<MTLDevice> preferredDevice;
|
||||||
|
@property(nonatomic) MTLPixelFormat colorPixelFormat;
|
||||||
|
@property(nonatomic) CGColorSpaceRef colorspace;
|
||||||
|
@property(nonatomic) BOOL framebufferOnly;
|
||||||
|
@property(nonatomic) CGSize drawableSize;
|
||||||
|
@property(nonatomic, readonly) CGSize preferredDrawableSize;
|
||||||
|
@property(nonatomic) BOOL autoResizeDrawable;
|
||||||
|
@property(nonatomic) MTLClearColor clearColor;
|
||||||
|
@property(nonatomic) MTLPixelFormat depthStencilPixelFormat;
|
||||||
|
@property(nonatomic) MTLTextureUsage depthStencilAttachmentTextureUsage;
|
||||||
|
@property(nonatomic) double clearDepth;
|
||||||
|
@property(nonatomic) uint32_t clearStencil;
|
||||||
|
@property(nonatomic) NSUInteger sampleCount;
|
||||||
|
@property(nonatomic) MTLTextureUsage multisampleColorAttachmentTextureUsage;
|
||||||
|
@property(nonatomic, readonly, nullable) MTLRenderPassDescriptor* currentRenderPassDescriptor;
|
||||||
|
@property(nonatomic, readonly, nullable) id<CAMetalDrawable> currentDrawable;
|
||||||
|
@property(nonatomic, readonly, nullable) id<MTLTexture> depthStencilTexture;
|
||||||
|
@property(nonatomic, readonly, nullable) id<MTLTexture> multisampleColorTexture;
|
||||||
|
@property(nonatomic) NSInteger preferredFramesPerSecond;
|
||||||
|
@property(nonatomic, getter=isPaused) BOOL paused;
|
||||||
|
@property(nonatomic) BOOL enableSetNeedsDisplay;
|
||||||
|
@property(nonatomic) BOOL presentsWithTransaction;
|
||||||
|
@property(nonatomic) MTLStorageMode depthStencilStorageMode;
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame: (NSRect)frame
|
||||||
|
device: (id<MTLDevice>)device;
|
||||||
|
- (void)draw;
|
||||||
|
- (void)releaseDrawables;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif // _METALKIT_MTKVIEW_H_
|
@ -20,6 +20,6 @@
|
|||||||
#ifndef _METALKIT_METALKIT_H_
|
#ifndef _METALKIT_METALKIT_H_
|
||||||
#define _METALKIT_METALKIT_H_
|
#define _METALKIT_METALKIT_H_
|
||||||
|
|
||||||
// TODO
|
#import <MetalKit/MTKView.h>
|
||||||
|
|
||||||
#endif // _METALKIT_METALKIT_H_
|
#endif // _METALKIT_METALKIT_H_
|
||||||
|
386
src/MetalKit/MTKView.m
Normal file
386
src/MetalKit/MTKView.m
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Darling.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 Darling developers
|
||||||
|
*
|
||||||
|
* Darling is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Darling is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <MetalKit/MTKView.h>
|
||||||
|
#import <Metal/stubs.h>
|
||||||
|
#import <Foundation/NSObjectInternal.h>
|
||||||
|
|
||||||
|
@implementation MTKView
|
||||||
|
|
||||||
|
#if __OBJC2__
|
||||||
|
|
||||||
|
{
|
||||||
|
id<MTKViewDelegate> _delegate;
|
||||||
|
NSInteger _preferredFramesPerSecond;
|
||||||
|
BOOL _paused;
|
||||||
|
BOOL _enableSetNeedsDisplay;
|
||||||
|
id<CAMetalDrawable> _currentDrawable;
|
||||||
|
NSTimer* _frameTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// properties
|
||||||
|
//
|
||||||
|
|
||||||
|
@synthesize autoResizeDrawable = _autoResizeDrawable;
|
||||||
|
@synthesize clearColor = _clearColor;
|
||||||
|
@synthesize depthStencilPixelFormat = _depthStencilPixelFormat;
|
||||||
|
@synthesize depthStencilAttachmentTextureUsage = _depthStencilAttachmentTextureUsage;
|
||||||
|
@synthesize depthStencilStorageMode = _depthStencilStorageMode;
|
||||||
|
@synthesize clearDepth = _clearDepth;
|
||||||
|
@synthesize clearStencil = _clearStencil;
|
||||||
|
@synthesize sampleCount = _sampleCount;
|
||||||
|
@synthesize multisampleColorAttachmentTextureUsage = _multisampleColorAttachmentTextureUsage;
|
||||||
|
@synthesize preferredFramesPerSecond = _preferredFramesPerSecond;
|
||||||
|
@synthesize paused = _paused;
|
||||||
|
@synthesize enableSetNeedsDisplay = _enableSetNeedsDisplay;
|
||||||
|
|
||||||
|
- (id<MTKViewDelegate>)delegate
|
||||||
|
{
|
||||||
|
return objc_loadWeak(&_delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDelegate: (id<MTKViewDelegate>)delegate
|
||||||
|
{
|
||||||
|
objc_storeWeak(&_delegate, delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<MTLDevice>)device
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).device;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDevice: (id<MTLDevice>)device
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<MTLDevice>)preferredDevice
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).preferredDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MTLPixelFormat)colorPixelFormat
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).pixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setColorPixelFormat: (MTLPixelFormat)colorPixelFormat
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).pixelFormat = colorPixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGColorSpaceRef)colorspace
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).colorspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setColorspace: (CGColorSpaceRef)colorspace
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).colorspace = colorspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)framebufferOnly
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).framebufferOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setFramebufferOnly: (BOOL)framebufferOnly
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).framebufferOnly = framebufferOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGSize)drawableSize
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).drawableSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDrawableSize: (CGSize)drawableSize
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).drawableSize = drawableSize;
|
||||||
|
[_delegate mtkView: self drawableSizeWillChange: drawableSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGSize)preferredDrawableSize
|
||||||
|
{
|
||||||
|
// TODO: multiply by contentsScale
|
||||||
|
return self.bounds.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MTLRenderPassDescriptor*)currentRenderPassDescriptor
|
||||||
|
{
|
||||||
|
id<MTLDevice> device = self.device;
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
id<CAMetalDrawable> drawable = self.currentDrawable;
|
||||||
|
|
||||||
|
if (!drawable) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
MTLRenderPassDescriptor* desc = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||||
|
|
||||||
|
desc.colorAttachments[0].texture = drawable.texture;
|
||||||
|
desc.colorAttachments[0].clearColor = _clearColor;
|
||||||
|
desc.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||||
|
desc.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||||
|
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<CAMetalDrawable>)currentDrawable
|
||||||
|
{
|
||||||
|
if (!_currentDrawable) {
|
||||||
|
_currentDrawable = [(CAMetalLayer*)_layer nextDrawable];
|
||||||
|
}
|
||||||
|
return _currentDrawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<MTLTexture>)depthStencilTexture
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<MTLTexture>)multisampleColorTexture
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the synthesized `preferredFramesPerSecond` getter
|
||||||
|
|
||||||
|
- (void)setPreferredFramesPerSecond: (NSInteger)preferredFramesPerSecond
|
||||||
|
{
|
||||||
|
if (preferredFramesPerSecond > 60) {
|
||||||
|
preferredFramesPerSecond = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredFramesPerSecond < 1) {
|
||||||
|
preferredFramesPerSecond = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredFramesPerSecond == _preferredFramesPerSecond) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_preferredFramesPerSecond = preferredFramesPerSecond;
|
||||||
|
|
||||||
|
[self _resetTimer];
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the synthesized `paused` getter
|
||||||
|
|
||||||
|
- (void)setPaused: (BOOL)paused
|
||||||
|
{
|
||||||
|
if (paused == _paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_paused = paused;
|
||||||
|
|
||||||
|
[self _resetTimer];
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the synthesized `enableSetNeedsDisplay` getter
|
||||||
|
|
||||||
|
- (void)setEnableSetNeedsDisplay: (BOOL)enableSetNeedsDisplay
|
||||||
|
{
|
||||||
|
if (enableSetNeedsDisplay == _enableSetNeedsDisplay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_enableSetNeedsDisplay = enableSetNeedsDisplay;
|
||||||
|
|
||||||
|
[self _resetTimer];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)presentsWithTransaction
|
||||||
|
{
|
||||||
|
return ((CAMetalLayer*)_layer).presentsWithTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setPresentsWithTransaction: (BOOL)presentsWithTransaction
|
||||||
|
{
|
||||||
|
((CAMetalLayer*)_layer).presentsWithTransaction = presentsWithTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// methods
|
||||||
|
//
|
||||||
|
|
||||||
|
- (void)_initCommon
|
||||||
|
{
|
||||||
|
_autoResizeDrawable = YES;
|
||||||
|
_clearColor = MTLClearColorMake(0, 0, 0, 1);
|
||||||
|
_depthStencilPixelFormat = MTLPixelFormatInvalid;
|
||||||
|
_depthStencilAttachmentTextureUsage = MTLTextureUsageRenderTarget;
|
||||||
|
_depthStencilStorageMode = MTLStorageModeShared; // TODO: check what the actual default is
|
||||||
|
_clearDepth = 1;
|
||||||
|
_clearStencil = 0;
|
||||||
|
_sampleCount = 1;
|
||||||
|
_multisampleColorAttachmentTextureUsage = MTLTextureUsageRenderTarget;
|
||||||
|
_preferredFramesPerSecond = 60;
|
||||||
|
_paused = NO;
|
||||||
|
_enableSetNeedsDisplay = NO;
|
||||||
|
|
||||||
|
[self _resetTimer];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithCoder: (NSCoder*)decoder
|
||||||
|
{
|
||||||
|
self.wantsLayer = YES;
|
||||||
|
// TODO: check if MTKView has any coding stuff of its own
|
||||||
|
self = [super initWithCoder: decoder];
|
||||||
|
if (self != nil) {
|
||||||
|
[self _initCommon];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame: (NSRect)frame
|
||||||
|
{
|
||||||
|
self.wantsLayer = YES;
|
||||||
|
self = [super initWithFrame: frame];
|
||||||
|
if (self != nil) {
|
||||||
|
[self _initCommon];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame: (NSRect)frame
|
||||||
|
device: (id<MTLDevice>)device
|
||||||
|
{
|
||||||
|
self.wantsLayer = YES;
|
||||||
|
self = [super initWithFrame: frame];
|
||||||
|
if (self != nil) {
|
||||||
|
self.device = device;
|
||||||
|
[self _initCommon];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
objc_storeWeak(&_delegate, nil);
|
||||||
|
[_currentDrawable release];
|
||||||
|
[_frameTimer invalidate];
|
||||||
|
[_frameTimer release];
|
||||||
|
[super dealloc];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CALayer*)makeBackingLayer
|
||||||
|
{
|
||||||
|
return [CAMetalLayer layer];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)draw
|
||||||
|
{
|
||||||
|
[_currentDrawable release];
|
||||||
|
_currentDrawable = [(CAMetalLayer*)_layer nextDrawable];
|
||||||
|
|
||||||
|
if ([self methodForSelector: @selector(drawRect:)] != [MTKView instanceMethodForSelector: @selector(drawRect:)]) {
|
||||||
|
// a subclass has overridden this method; invoke it.
|
||||||
|
[self drawRect: self.bounds];
|
||||||
|
} else {
|
||||||
|
// `drawRect:` has not been overridden; invoke the delegate's method.
|
||||||
|
[_delegate drawInMTKView: self];
|
||||||
|
}
|
||||||
|
|
||||||
|
[_currentDrawable release];
|
||||||
|
_currentDrawable = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)releaseDrawables
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
// we don't need this yet since we don't have depth, stencil, or multisample textures
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_resetTimer
|
||||||
|
{
|
||||||
|
[_frameTimer invalidate];
|
||||||
|
[_frameTimer release];
|
||||||
|
|
||||||
|
if (_paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_NSWeakRef* weakSelf = [[[_NSWeakRef alloc] initWithObject: self] autorelease];
|
||||||
|
|
||||||
|
_frameTimer = [[NSTimer scheduledTimerWithTimeInterval: 1.0 / _preferredFramesPerSecond
|
||||||
|
repeats: YES
|
||||||
|
block: ^(NSTimer* timer) {
|
||||||
|
MTKView* me = weakSelf.object;
|
||||||
|
[me draw];
|
||||||
|
}] retain];
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this is a lazy (and probably half-assed) implementation.
|
||||||
|
// currently, subclassing an MTKView and implementing `drawRect:`
|
||||||
|
// will result in the method always being invoked after `setNeedsDisplay:`,
|
||||||
|
// regardless of whether or not `enableSetNeedsDisplay` is set.
|
||||||
|
//
|
||||||
|
// we need to check what the official behavior regarding this method
|
||||||
|
// is with subclasses: we need to check whether `drawRect:` is always
|
||||||
|
// invoked from `setNeedsDisplay:` or if setting `enableSetNeedsDisplay`
|
||||||
|
// to NO will prevent the subclass' `drawRect:` from being invoked due
|
||||||
|
// to `setNeedsDisplay`.
|
||||||
|
//
|
||||||
|
// the reason i didn't go ahead and do what sounds like the right behavior
|
||||||
|
// right now is because that would requiring overridding various display-related
|
||||||
|
// methods, not just `setNeedsDisplay`.
|
||||||
|
- (void)drawRect: (NSRect)rect
|
||||||
|
{
|
||||||
|
// if we're not paused, we only redraw when the timer fires.
|
||||||
|
// if we're not supposed to respond to setNeedsDisplay, we only redraw via manual calls of `draw`.
|
||||||
|
if (!_paused || !_enableSetNeedsDisplay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self draw];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setFrame: (NSRect)frame
|
||||||
|
{
|
||||||
|
[super setFrame: frame];
|
||||||
|
|
||||||
|
if (_autoResizeDrawable) {
|
||||||
|
self.drawableSize = _frame.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setBounds: (NSRect)bounds
|
||||||
|
{
|
||||||
|
[super setBounds: bounds];
|
||||||
|
|
||||||
|
if (_autoResizeDrawable) {
|
||||||
|
self.drawableSize = _bounds.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
MTL_UNSUPPORTED_CLASS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@end
|
Loading…
Reference in New Issue
Block a user