mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 02:15:34 +00:00
gpu,app/internal/wm: add Metal port
The OpenGL (ES) implementations on Apple platforms are deprecated and don't support GPU compute programs. This change adds support for the replacement, the Metal GPU API. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+14
-10
@@ -1,11 +1,13 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build darwin && ios
|
||||
// +build darwin,ios
|
||||
//go:build darwin && ios && nometal
|
||||
// +build darwin,ios,nometal
|
||||
|
||||
package wm
|
||||
|
||||
/*
|
||||
@import UIKit;
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <OpenGLES/ES2/gl.h>
|
||||
#include <OpenGLES/ES2/glext.h>
|
||||
@@ -15,6 +17,14 @@ __attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ct
|
||||
__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx);
|
||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void);
|
||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void);
|
||||
|
||||
static CFTypeRef getViewLayer(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
return CFBridgingRetain(view.layer);
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -36,12 +46,6 @@ type context struct {
|
||||
colorBuffer gl.Renderbuffer
|
||||
}
|
||||
|
||||
func init() {
|
||||
layerFactory = func() uintptr {
|
||||
return uintptr(C.gio_createGLLayer())
|
||||
}
|
||||
}
|
||||
|
||||
func newContext(w *window) (*context, error) {
|
||||
ctx := C.gio_createContext()
|
||||
if ctx == 0 {
|
||||
@@ -55,7 +59,7 @@ func newContext(w *window) (*context, error) {
|
||||
c := &context{
|
||||
ctx: ctx,
|
||||
owner: w,
|
||||
layer: C.CFTypeRef(w.contextLayer()),
|
||||
layer: C.getViewLayer(w.contextView()),
|
||||
c: f,
|
||||
}
|
||||
return c, nil
|
||||
@@ -116,7 +120,7 @@ func (c *context) Refresh() error {
|
||||
c.frameBuffer = c.c.CreateFramebuffer()
|
||||
c.colorBuffer = c.c.CreateRenderbuffer()
|
||||
}
|
||||
if !c.owner.isVisible() {
|
||||
if !c.owner.visible {
|
||||
// Make sure any in-flight GL commands are complete.
|
||||
c.c.Finish()
|
||||
return nil
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// +build darwin,ios
|
||||
// +build darwin,ios,nometal
|
||||
|
||||
@import UIKit;
|
||||
@import OpenGLES;
|
||||
|
||||
#include "_cgo_export.h"
|
||||
|
||||
Class gio_layerClass(void) {
|
||||
return [CAEAGLLayer class];
|
||||
}
|
||||
|
||||
int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) {
|
||||
EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
|
||||
CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build darwin && !ios
|
||||
// +build darwin,!ios
|
||||
//go:build darwin && !ios && nometal
|
||||
// +build darwin,!ios,nometal
|
||||
|
||||
package wm
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// +build darwin,!ios
|
||||
// +build darwin,!ios,nometal
|
||||
|
||||
@import AppKit;
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#include "_cgo_export.h"
|
||||
|
||||
CALayer *gio_layerFactory(void) {
|
||||
@autoreleasepool {
|
||||
return [CALayer layer];
|
||||
}
|
||||
}
|
||||
|
||||
CFTypeRef gio_createGLContext(void) {
|
||||
@autoreleasepool {
|
||||
NSOpenGLPixelFormatAttribute attr[] = {
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build !nometal
|
||||
// +build !nometal
|
||||
|
||||
package wm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/gpu"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc
|
||||
|
||||
@import Metal;
|
||||
@import QuartzCore.CAMetalLayer;
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
static CFTypeRef createMetalDevice(void) {
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
return CFBridgingRetain(dev);
|
||||
}
|
||||
}
|
||||
|
||||
static void setupLayer(CFTypeRef layerRef, CFTypeRef devRef) {
|
||||
@autoreleasepool {
|
||||
CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
|
||||
id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
|
||||
layer.device = dev;
|
||||
// Package gpu assumes an sRGB-encoded framebuffer.
|
||||
layer.pixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
// Never let nextDrawable time out and return nil.
|
||||
layer.allowsNextDrawableTimeout = NO;
|
||||
}
|
||||
if (@available(iOS 11.2, *)) {
|
||||
// Minimize latency from input to window and save a bit of memory.
|
||||
layer.maximumDrawableCount = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static CFTypeRef nextDrawable(CFTypeRef layerRef) {
|
||||
@autoreleasepool {
|
||||
CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
|
||||
return CFBridgingRetain([layer nextDrawable]);
|
||||
}
|
||||
}
|
||||
|
||||
static CFTypeRef drawableTexture(CFTypeRef drawableRef) {
|
||||
@autoreleasepool {
|
||||
id<CAMetalDrawable> drawable = (__bridge id<CAMetalDrawable>)drawableRef;
|
||||
return CFBridgingRetain(drawable.texture);
|
||||
}
|
||||
}
|
||||
|
||||
static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
|
||||
@autoreleasepool {
|
||||
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
|
||||
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
|
||||
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
|
||||
[cmdBuffer presentDrawable:drawable];
|
||||
[cmdBuffer commit];
|
||||
[cmdBuffer waitUntilCompleted];
|
||||
}
|
||||
}
|
||||
|
||||
static CFTypeRef newCommandQueue(CFTypeRef devRef) {
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
|
||||
return CFBridgingRetain([dev newCommandQueue]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type mtlContext struct {
|
||||
dev C.CFTypeRef
|
||||
view C.CFTypeRef
|
||||
layer C.CFTypeRef
|
||||
queue C.CFTypeRef
|
||||
drawable C.CFTypeRef
|
||||
texture C.CFTypeRef
|
||||
}
|
||||
|
||||
func newMtlContext(w *window) (*mtlContext, error) {
|
||||
dev := C.createMetalDevice()
|
||||
if dev == 0 {
|
||||
return nil, errors.New("metal: MTLCreateSystemDefaultDevice failed")
|
||||
}
|
||||
view := w.contextView()
|
||||
layer := getMetalLayer(view)
|
||||
if layer == 0 {
|
||||
C.CFRelease(dev)
|
||||
return nil, errors.New("metal: CAMetalLayer construction failed")
|
||||
}
|
||||
queue := C.newCommandQueue(dev)
|
||||
if layer == 0 {
|
||||
C.CFRelease(dev)
|
||||
C.CFRelease(layer)
|
||||
return nil, errors.New("metal: [MTLDevice newCommandQueue] failed")
|
||||
}
|
||||
C.setupLayer(layer, dev)
|
||||
c := &mtlContext{
|
||||
dev: dev,
|
||||
view: view,
|
||||
layer: layer,
|
||||
queue: queue,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *mtlContext) RenderTarget() gpu.RenderTarget {
|
||||
if c.drawable != 0 || c.texture != 0 {
|
||||
panic("a previous RenderTarget wasn't Presented")
|
||||
}
|
||||
c.drawable = C.nextDrawable(c.layer)
|
||||
if c.drawable == 0 {
|
||||
panic("metal: [CAMetalLayer nextDrawable] failed")
|
||||
}
|
||||
c.texture = C.drawableTexture(c.drawable)
|
||||
if c.texture == 0 {
|
||||
panic("metal: CADrawable.texture is nil")
|
||||
}
|
||||
return gpu.MetalRenderTarget{
|
||||
Texture: unsafe.Pointer(c.texture),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mtlContext) API() gpu.API {
|
||||
return gpu.Metal{
|
||||
Device: unsafe.Pointer(c.dev),
|
||||
Queue: unsafe.Pointer(c.queue),
|
||||
PixelFormat: int(C.MTLPixelFormatBGRA8Unorm_sRGB),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mtlContext) Release() {
|
||||
C.CFRelease(c.queue)
|
||||
C.CFRelease(c.dev)
|
||||
C.CFRelease(c.layer)
|
||||
if c.drawable != 0 {
|
||||
C.CFRelease(c.drawable)
|
||||
}
|
||||
if c.texture != 0 {
|
||||
C.CFRelease(c.texture)
|
||||
}
|
||||
*c = mtlContext{}
|
||||
}
|
||||
|
||||
func (c *mtlContext) Present() error {
|
||||
C.CFRelease(c.texture)
|
||||
c.texture = 0
|
||||
C.presentDrawable(c.queue, c.drawable)
|
||||
C.CFRelease(c.drawable)
|
||||
c.drawable = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mtlContext) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mtlContext) Unlock() {}
|
||||
|
||||
func (c *mtlContext) Refresh() error {
|
||||
resizeDrawable(c.view, c.layer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *window) NewContext() (Context, error) {
|
||||
return newMtlContext(w)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build !nometal
|
||||
// +build !nometal
|
||||
|
||||
package wm
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@import QuartzCore.CAMetalLayer;
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
Class gio_layerClass(void) {
|
||||
return [CAMetalLayer class];
|
||||
}
|
||||
|
||||
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
return CFBridgingRetain(view.layer);
|
||||
}
|
||||
}
|
||||
|
||||
static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) {
|
||||
@autoreleasepool {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
|
||||
layer.contentsScale = view.contentScaleFactor;
|
||||
CGSize size = layer.bounds.size;
|
||||
size.width *= layer.contentsScale;
|
||||
size.height *= layer.contentsScale;
|
||||
layer.drawableSize = size;
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func getMetalLayer(view C.CFTypeRef) C.CFTypeRef {
|
||||
return C.getMetalLayer(view)
|
||||
}
|
||||
|
||||
func resizeDrawable(view, layer C.CFTypeRef) {
|
||||
C.resizeDrawable(view, layer)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build darwin && !ios && !nometal
|
||||
// +build darwin,!ios,!nometal
|
||||
|
||||
package wm
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc
|
||||
|
||||
@import AppKit;
|
||||
|
||||
@import QuartzCore.CAMetalLayer;
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
CALayer *gio_layerFactory(void) {
|
||||
@autoreleasepool {
|
||||
return [CAMetalLayer layer];
|
||||
}
|
||||
}
|
||||
|
||||
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return CFBridgingRetain(view.layer);
|
||||
}
|
||||
}
|
||||
|
||||
static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
|
||||
CGSize size = layer.bounds.size;
|
||||
size.width *= layer.contentsScale;
|
||||
size.height *= layer.contentsScale;
|
||||
layer.drawableSize = size;
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func getMetalLayer(view C.CFTypeRef) C.CFTypeRef {
|
||||
return C.getMetalLayer(view)
|
||||
}
|
||||
|
||||
func resizeDrawable(view, layer C.CFTypeRef) {
|
||||
C.resizeDrawable(view, layer)
|
||||
}
|
||||
@@ -46,24 +46,6 @@ static void hideTextInput(CFTypeRef viewRef) {
|
||||
[view resignFirstResponder];
|
||||
}
|
||||
|
||||
static void addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
CALayer *layer = (__bridge CALayer *)layerRef;
|
||||
[view.layer addSublayer:layer];
|
||||
}
|
||||
|
||||
static void updateView(CFTypeRef viewRef, CFTypeRef layerRef) {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
|
||||
layer.contentsScale = view.contentScaleFactor;
|
||||
layer.bounds = view.bounds;
|
||||
}
|
||||
|
||||
static void removeLayer(CFTypeRef layerRef) {
|
||||
CALayer *layer = (__bridge CALayer *)layerRef;
|
||||
[layer removeFromSuperlayer];
|
||||
}
|
||||
|
||||
static struct drawParams viewDrawParams(CFTypeRef viewRef) {
|
||||
UIView *v = (__bridge UIView *)viewRef;
|
||||
struct drawParams params;
|
||||
@@ -92,7 +74,6 @@ import (
|
||||
"image"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
@@ -112,8 +93,7 @@ type window struct {
|
||||
w Callbacks
|
||||
displayLink *displayLink
|
||||
|
||||
layer C.CFTypeRef
|
||||
visible atomic.Value
|
||||
visible bool
|
||||
cursor pointer.CursorName
|
||||
|
||||
pointerMap []C.CFTypeRef
|
||||
@@ -121,8 +101,6 @@ type window struct {
|
||||
|
||||
var mainWindow = newWindowRendezvous()
|
||||
|
||||
var layerFactory func() uintptr
|
||||
|
||||
var views = make(map[C.CFTypeRef]*window)
|
||||
|
||||
func init() {
|
||||
@@ -145,9 +123,6 @@ func onCreate(view C.CFTypeRef) {
|
||||
wopts := <-mainWindow.out
|
||||
w.w = wopts.window
|
||||
w.w.SetDriver(w)
|
||||
w.visible.Store(false)
|
||||
w.layer = C.CFTypeRef(layerFactory())
|
||||
C.addLayerToView(view, w.layer)
|
||||
views[view] = w
|
||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
||||
}
|
||||
@@ -163,9 +138,8 @@ func (w *window) draw(sync bool) {
|
||||
if params.width == 0 || params.height == 0 {
|
||||
return
|
||||
}
|
||||
wasVisible := w.isVisible()
|
||||
w.visible.Store(true)
|
||||
C.updateView(w.view, w.layer)
|
||||
wasVisible := w.visible
|
||||
w.visible = true
|
||||
if !wasVisible {
|
||||
w.w.Event(system.StageEvent{Stage: system.StageRunning})
|
||||
}
|
||||
@@ -195,7 +169,7 @@ func (w *window) draw(sync bool) {
|
||||
//export onStop
|
||||
func onStop(view C.CFTypeRef) {
|
||||
w := views[view]
|
||||
w.visible.Store(false)
|
||||
w.visible = false
|
||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
||||
}
|
||||
|
||||
@@ -205,9 +179,6 @@ func onDestroy(view C.CFTypeRef) {
|
||||
delete(views, view)
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
w.displayLink.Close()
|
||||
C.removeLayer(w.layer)
|
||||
C.CFRelease(w.layer)
|
||||
w.layer = 0
|
||||
w.view = 0
|
||||
}
|
||||
|
||||
@@ -341,12 +312,8 @@ func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID {
|
||||
return pointer.ID(id)
|
||||
}
|
||||
|
||||
func (w *window) contextLayer() uintptr {
|
||||
return uintptr(w.layer)
|
||||
}
|
||||
|
||||
func (w *window) isVisible() bool {
|
||||
return w.visible.Load().(bool)
|
||||
func (w *window) contextView() C.CFTypeRef {
|
||||
return w.view
|
||||
}
|
||||
|
||||
func (w *window) ShowTextInput(show bool) {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "_cgo_export.h"
|
||||
#include "framework_ios.h"
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
|
||||
|
||||
@interface GioView: UIView <UIKeyInput>
|
||||
@end
|
||||
|
||||
@@ -125,6 +127,9 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
gio_onFrameCallback((__bridge CFTypeRef)link);
|
||||
}
|
||||
|
||||
+ (Class)layerClass {
|
||||
return gio_layerClass();
|
||||
}
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
if (self.window != nil) {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||
@@ -157,9 +162,6 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
handleTouches(0, self, touches, event);
|
||||
}
|
||||
|
||||
@@ -423,11 +423,9 @@ func gio_onClose(view C.CFTypeRef) {
|
||||
deleteView(view)
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
w.displayLink.Close()
|
||||
w.displayLink = nil
|
||||
C.CFRelease(w.view)
|
||||
w.view = 0
|
||||
C.CFRelease(w.window)
|
||||
w.window = 0
|
||||
*w = window{}
|
||||
}
|
||||
|
||||
//export gio_onHide
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "_cgo_export.h"
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
|
||||
|
||||
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
|
||||
@end
|
||||
|
||||
@@ -54,13 +56,24 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
||||
gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
||||
}
|
||||
|
||||
@interface GioView : NSView
|
||||
@interface GioView : NSView <CALayerDelegate>
|
||||
@end
|
||||
|
||||
@implementation GioView
|
||||
// drawRect is called when OpenGL is used, displayLayer otherwise.
|
||||
// Don't know why.
|
||||
- (void)drawRect:(NSRect)r {
|
||||
gio_onDraw((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (void)displayLayer:(CALayer *)layer {
|
||||
layer.contentsScale = self.window.backingScaleFactor;
|
||||
gio_onDraw((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (CALayer *)makeBackingLayer {
|
||||
CALayer *layer = gio_layerFactory();
|
||||
layer.delegate = self;
|
||||
return layer;
|
||||
}
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||
}
|
||||
@@ -221,6 +234,7 @@ CFTypeRef gio_createView(void) {
|
||||
NSRect frame = NSMakeRect(0, 0, 0, 0);
|
||||
GioView* view = [[GioView alloc] initWithFrame:frame];
|
||||
[view setWantsLayer:YES];
|
||||
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
||||
return CFBridgingRetain(view);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user