Support hidpi in Cairo GAL canvas; performance improvements.
We now draw onto wxBitmap directly, reducing the amount of copying. Also moves the bitmap blit into paint event handler. Modifies ClearScreen to work universally between Cairo/OpenGL backends.
This commit is contained in:
parent
31a0652c58
commit
8e90063258
|
@ -2,7 +2,7 @@
|
||||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2013-2017 CERN
|
* Copyright (C) 2013-2017 CERN
|
||||||
* Copyright (C) 2013-2023, 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
* Copyright (C) 2013-2024, 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||||
*
|
*
|
||||||
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
|
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
|
||||||
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
||||||
|
@ -281,20 +281,10 @@ bool EDA_DRAW_PANEL_GAL::DoRePaint()
|
||||||
m_gal->SetGridColor( settings->GetGridColor() );
|
m_gal->SetGridColor( settings->GetGridColor() );
|
||||||
m_gal->SetCursorColor( settings->GetCursorColor() );
|
m_gal->SetCursorColor( settings->GetCursorColor() );
|
||||||
|
|
||||||
// TODO: find why ClearScreen() must be called here in opengl mode
|
m_gal->ClearScreen();
|
||||||
// and only if m_view->IsDirty() in Cairo mode to avoid display artifacts
|
|
||||||
// when moving the mouse cursor
|
|
||||||
if( m_backend == GAL_TYPE_OPENGL )
|
|
||||||
m_gal->ClearScreen();
|
|
||||||
|
|
||||||
if( m_view->IsDirty() )
|
if( m_view->IsDirty() )
|
||||||
{
|
{
|
||||||
if( m_backend != GAL_TYPE_OPENGL // Already called in opengl
|
|
||||||
&& m_view->IsTargetDirty( KIGFX::TARGET_NONCACHED ) )
|
|
||||||
{
|
|
||||||
m_gal->ClearScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_view->ClearTargets();
|
m_view->ClearTargets();
|
||||||
|
|
||||||
// Grid has to be redrawn only when the NONCACHED target is redrawn
|
// Grid has to be redrawn only when the NONCACHED target is redrawn
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* This program source code file is part of KICAD, a free EDA CAD application.
|
* This program source code file is part of KICAD, a free EDA CAD application.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
||||||
* Copyright (C) 2012-2023 Kicad Developers, see AUTHORS.txt for contributors.
|
* Copyright (C) 2012-2024 Kicad Developers, see AUTHORS.txt for contributors.
|
||||||
* Copyright (C) 2017-2018 CERN
|
* Copyright (C) 2017-2018 CERN
|
||||||
*
|
*
|
||||||
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
||||||
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#include <wx/image.h>
|
#include <wx/image.h>
|
||||||
#include <wx/log.h>
|
#include <wx/log.h>
|
||||||
|
#include <wx/rawbmp.h>
|
||||||
|
|
||||||
#include <gal/cairo/cairo_gal.h>
|
#include <gal/cairo/cairo_gal.h>
|
||||||
#include <gal/cairo/cairo_compositor.h>
|
#include <gal/cairo/cairo_compositor.h>
|
||||||
|
@ -583,6 +584,9 @@ void CAIRO_GAL_BASE::DrawBitmap( const BITMAP_BASE& aBitmap, double alphaBlend )
|
||||||
void CAIRO_GAL_BASE::ResizeScreen( int aWidth, int aHeight )
|
void CAIRO_GAL_BASE::ResizeScreen( int aWidth, int aHeight )
|
||||||
{
|
{
|
||||||
m_screenSize = VECTOR2I( aWidth, aHeight );
|
m_screenSize = VECTOR2I( aWidth, aHeight );
|
||||||
|
|
||||||
|
m_bitmapSize = VECTOR2I( std::ceil( m_screenSize.x * getScalingFactor() ),
|
||||||
|
std::ceil( m_screenSize.y * getScalingFactor() ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -594,9 +598,9 @@ void CAIRO_GAL_BASE::Flush()
|
||||||
|
|
||||||
void CAIRO_GAL_BASE::ClearScreen()
|
void CAIRO_GAL_BASE::ClearScreen()
|
||||||
{
|
{
|
||||||
cairo_set_source_rgb( m_currentContext, m_clearColor.r, m_clearColor.g, m_clearColor.b );
|
cairo_set_source_rgb( m_context, m_clearColor.r, m_clearColor.g, m_clearColor.b );
|
||||||
cairo_rectangle( m_currentContext, 0.0, 0.0, m_screenSize.x, m_screenSize.y );
|
cairo_rectangle( m_context, 0.0, 0.0, m_bitmapSize.x, m_bitmapSize.y );
|
||||||
cairo_fill( m_currentContext );
|
cairo_fill( m_context );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1021,15 +1025,15 @@ void CAIRO_GAL_BASE::resetContext()
|
||||||
|
|
||||||
m_imageSurfaces.clear();
|
m_imageSurfaces.clear();
|
||||||
|
|
||||||
ClearScreen();
|
|
||||||
|
|
||||||
// Compute the world <-> screen transformations
|
// Compute the world <-> screen transformations
|
||||||
ComputeWorldScreenMatrix();
|
ComputeWorldScreenMatrix();
|
||||||
|
|
||||||
cairo_matrix_init( &m_cairoWorldScreenMatrix, m_worldScreenMatrix.m_data[0][0],
|
double sf = getScalingFactor();
|
||||||
m_worldScreenMatrix.m_data[1][0], m_worldScreenMatrix.m_data[0][1],
|
|
||||||
m_worldScreenMatrix.m_data[1][1], m_worldScreenMatrix.m_data[0][2],
|
cairo_matrix_init( &m_cairoWorldScreenMatrix,
|
||||||
m_worldScreenMatrix.m_data[1][2] );
|
m_worldScreenMatrix.m_data[0][0] * sf, m_worldScreenMatrix.m_data[1][0] * sf,
|
||||||
|
m_worldScreenMatrix.m_data[0][1] * sf, m_worldScreenMatrix.m_data[1][1] * sf,
|
||||||
|
m_worldScreenMatrix.m_data[0][2] * sf, m_worldScreenMatrix.m_data[1][2] * sf );
|
||||||
|
|
||||||
// we work in screen-space coordinates and do the transforms outside.
|
// we work in screen-space coordinates and do the transforms outside.
|
||||||
cairo_identity_matrix( m_context );
|
cairo_identity_matrix( m_context );
|
||||||
|
@ -1337,8 +1341,10 @@ CAIRO_GAL::CAIRO_GAL( GAL_DISPLAY_OPTIONS& aDisplayOptions, wxWindow* aParent,
|
||||||
m_currentTarget = TARGET_NONCACHED;
|
m_currentTarget = TARGET_NONCACHED;
|
||||||
SetTarget( TARGET_NONCACHED );
|
SetTarget( TARGET_NONCACHED );
|
||||||
|
|
||||||
|
SetBackgroundStyle( wxBG_STYLE_PAINT );
|
||||||
|
|
||||||
m_bitmapBuffer = nullptr;
|
m_bitmapBuffer = nullptr;
|
||||||
m_wxOutput = nullptr;
|
m_wxBitmap = nullptr;
|
||||||
|
|
||||||
m_parentWindow = aParent;
|
m_parentWindow = aParent;
|
||||||
m_mouseListener = aMouseListener;
|
m_mouseListener = aMouseListener;
|
||||||
|
@ -1374,10 +1380,6 @@ CAIRO_GAL::CAIRO_GAL( GAL_DISPLAY_OPTIONS& aDisplayOptions, wxWindow* aParent,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SetSize( aParent->GetClientSize() );
|
SetSize( aParent->GetClientSize() );
|
||||||
m_screenSize = ToVECTOR2I( aParent->GetClientSize() );
|
|
||||||
|
|
||||||
// Allocate memory for pixel storage
|
|
||||||
allocateBitmaps();
|
|
||||||
|
|
||||||
m_isInitialized = false;
|
m_isInitialized = false;
|
||||||
}
|
}
|
||||||
|
@ -1413,45 +1415,44 @@ void CAIRO_GAL::EndDrawing()
|
||||||
|
|
||||||
// Now translate the raw context data from the format stored
|
// Now translate the raw context data from the format stored
|
||||||
// by cairo into a format understood by wxImage.
|
// by cairo into a format understood by wxImage.
|
||||||
int height = m_screenSize.y;
|
int height = m_bitmapSize.y;
|
||||||
int stride = m_stride;
|
int stride = m_stride;
|
||||||
|
|
||||||
unsigned char* srcRow = m_bitmapBuffer;
|
unsigned char* srcRow = m_bitmapBuffer;
|
||||||
unsigned char* dst = m_wxOutput;
|
|
||||||
|
wxNativePixelData dstData( *m_wxBitmap );
|
||||||
|
wxNativePixelData::Iterator di( dstData );
|
||||||
|
|
||||||
for( int y = 0; y < height; y++ )
|
for( int y = 0; y < height; y++ )
|
||||||
{
|
{
|
||||||
for( int x = 0; x < stride; x += 4 )
|
wxNativePixelData::Iterator rowStart = di;
|
||||||
|
|
||||||
|
for( int x = 0; x < stride; x += 4, ++di )
|
||||||
{
|
{
|
||||||
const unsigned char* src = srcRow + x;
|
const unsigned char* src = srcRow + x;
|
||||||
|
|
||||||
#if defined( __BYTE_ORDER__ ) && ( __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ )
|
#if defined( __BYTE_ORDER__ ) && ( __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ )
|
||||||
// XRGB
|
// XRGB
|
||||||
dst[0] = src[1];
|
di.Red() = src[1];
|
||||||
dst[1] = src[2];
|
di.Green() = src[2];
|
||||||
dst[2] = src[3];
|
di.Blue() = src[3];
|
||||||
#else
|
#else
|
||||||
// BGRX
|
// BGRX
|
||||||
dst[0] = src[2];
|
di.Red() = src[2];
|
||||||
dst[1] = src[1];
|
di.Green() = src[1];
|
||||||
dst[2] = src[0];
|
di.Blue() = src[0];
|
||||||
#endif
|
#endif
|
||||||
dst += 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRow += stride;
|
srcRow += stride;
|
||||||
|
|
||||||
|
di = rowStart;
|
||||||
|
di.OffsetY( dstData, 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
wxImage img( m_wxBufferWidth, m_screenSize.y, m_wxOutput, true );
|
|
||||||
wxBitmap bmp( img );
|
|
||||||
wxMemoryDC mdc( bmp );
|
|
||||||
wxClientDC clientDC( this );
|
|
||||||
|
|
||||||
// Now it is the time to blit the mouse cursor
|
|
||||||
blitCursor( mdc );
|
|
||||||
clientDC.Blit( 0, 0, m_screenSize.x, m_screenSize.y, &mdc, 0, 0, wxCOPY );
|
|
||||||
|
|
||||||
deinitSurface();
|
deinitSurface();
|
||||||
|
|
||||||
|
Refresh(); // Trigger repaint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1472,7 +1473,7 @@ void CAIRO_GAL::ResizeScreen( int aWidth, int aHeight )
|
||||||
allocateBitmaps();
|
allocateBitmaps();
|
||||||
|
|
||||||
if( m_validCompositor )
|
if( m_validCompositor )
|
||||||
m_compositor->Resize( aWidth, aHeight );
|
m_compositor->Resize( m_bitmapSize.x, m_bitmapSize.y );
|
||||||
|
|
||||||
m_validCompositor = false;
|
m_validCompositor = false;
|
||||||
|
|
||||||
|
@ -1563,7 +1564,7 @@ void CAIRO_GAL::initSurface()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_surface = cairo_image_surface_create_for_data( m_bitmapBuffer, GAL_FORMAT, m_wxBufferWidth,
|
m_surface = cairo_image_surface_create_for_data( m_bitmapBuffer, GAL_FORMAT, m_wxBufferWidth,
|
||||||
m_screenSize.y, m_stride );
|
m_bitmapSize.y, m_stride );
|
||||||
|
|
||||||
m_context = cairo_create( m_surface );
|
m_context = cairo_create( m_surface );
|
||||||
|
|
||||||
|
@ -1594,17 +1595,18 @@ void CAIRO_GAL::deinitSurface()
|
||||||
|
|
||||||
void CAIRO_GAL::allocateBitmaps()
|
void CAIRO_GAL::allocateBitmaps()
|
||||||
{
|
{
|
||||||
m_wxBufferWidth = m_screenSize.x;
|
m_wxBufferWidth = m_bitmapSize.x;
|
||||||
|
|
||||||
// Create buffer, use the system independent Cairo context backend
|
// Create buffer, use the system independent Cairo context backend
|
||||||
m_stride = cairo_format_stride_for_width( GAL_FORMAT, m_wxBufferWidth );
|
m_stride = cairo_format_stride_for_width( GAL_FORMAT, m_wxBufferWidth );
|
||||||
m_bufferSize = m_stride * m_screenSize.y;
|
m_bufferSize = m_stride * m_bitmapSize.y;
|
||||||
|
|
||||||
wxASSERT( m_bitmapBuffer == nullptr );
|
wxASSERT( m_bitmapBuffer == nullptr );
|
||||||
m_bitmapBuffer = new unsigned char[m_bufferSize];
|
m_bitmapBuffer = new unsigned char[m_bufferSize];
|
||||||
|
|
||||||
wxASSERT( m_wxOutput == nullptr );
|
wxASSERT( m_wxBitmap == nullptr );
|
||||||
m_wxOutput = new unsigned char[m_wxBufferWidth * 3 * m_screenSize.y];
|
m_wxBitmap = new wxBitmap( m_wxBufferWidth, m_bitmapSize.y, 24 );
|
||||||
|
m_wxBitmap->SetScaleFactor( getScalingFactor() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1613,8 +1615,8 @@ void CAIRO_GAL::deleteBitmaps()
|
||||||
delete[] m_bitmapBuffer;
|
delete[] m_bitmapBuffer;
|
||||||
m_bitmapBuffer = nullptr;
|
m_bitmapBuffer = nullptr;
|
||||||
|
|
||||||
delete[] m_wxOutput;
|
delete m_wxBitmap;
|
||||||
m_wxOutput = nullptr;
|
m_wxBitmap = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1622,7 +1624,7 @@ void CAIRO_GAL::setCompositor()
|
||||||
{
|
{
|
||||||
// Recreate the compositor with the new Cairo context
|
// Recreate the compositor with the new Cairo context
|
||||||
m_compositor.reset( new CAIRO_COMPOSITOR( &m_currentContext ) );
|
m_compositor.reset( new CAIRO_COMPOSITOR( &m_currentContext ) );
|
||||||
m_compositor->Resize( m_screenSize.x, m_screenSize.y );
|
m_compositor->Resize( m_bitmapSize.x, m_bitmapSize.y );
|
||||||
m_compositor->SetAntialiasingMode( m_options.cairo_antialiasing_mode );
|
m_compositor->SetAntialiasingMode( m_options.cairo_antialiasing_mode );
|
||||||
|
|
||||||
// Prepare buffers
|
// Prepare buffers
|
||||||
|
@ -1636,7 +1638,22 @@ void CAIRO_GAL::setCompositor()
|
||||||
|
|
||||||
void CAIRO_GAL::onPaint( wxPaintEvent& aEvent )
|
void CAIRO_GAL::onPaint( wxPaintEvent& aEvent )
|
||||||
{
|
{
|
||||||
PostPaint( aEvent );
|
// We should have the rendered image in m_wxBitmap after EDA_DRAW_PANEL_GAL::onPaint
|
||||||
|
|
||||||
|
if( !m_wxBitmap )
|
||||||
|
{
|
||||||
|
wxLogDebug( "CAIRO_GAL::onPaint null output bitmap buffer" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Now it is the time to blit the mouse cursor
|
||||||
|
wxMemoryDC mdc( *m_wxBitmap );
|
||||||
|
blitCursor( mdc );
|
||||||
|
}
|
||||||
|
|
||||||
|
wxPaintDC paintDC( this );
|
||||||
|
paintDC.DrawBitmap( *m_wxBitmap, 0, 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1672,6 +1689,12 @@ bool CAIRO_GAL::updatedGalDisplayOptions( const GAL_DISPLAY_OPTIONS& aOptions )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
double CAIRO_GAL::getScalingFactor()
|
||||||
|
{
|
||||||
|
return GetContentScaleFactor();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
bool CAIRO_GAL::SetNativeCursorStyle( KICURSOR aCursor, bool aHiDPI )
|
bool CAIRO_GAL::SetNativeCursorStyle( KICURSOR aCursor, bool aHiDPI )
|
||||||
{
|
{
|
||||||
// Store the current cursor type and get the wxCursor for it
|
// Store the current cursor type and get the wxCursor for it
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* This program source code file is part of KICAD, a free EDA CAD application.
|
* This program source code file is part of KICAD, a free EDA CAD application.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
||||||
* Copyright (C) 2012-2021 KiCad Developers, see AUTHORS.txt for contributors.
|
* Copyright (C) 2012-2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||||
* Copyright (C) 2017-2018 CERN
|
* Copyright (C) 2017-2018 CERN
|
||||||
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
||||||
*
|
*
|
||||||
|
@ -252,6 +252,9 @@ protected:
|
||||||
const VECTOR2D xform( double x, double y ); // rotation, scale and offset
|
const VECTOR2D xform( double x, double y ); // rotation, scale and offset
|
||||||
const VECTOR2D xform( const VECTOR2D& aP ); // rotation, scale and offset
|
const VECTOR2D xform( const VECTOR2D& aP ); // rotation, scale and offset
|
||||||
|
|
||||||
|
// Return the scaling factor for current window.
|
||||||
|
virtual double getScalingFactor() { return 1.0; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform according to the rotation from m_currentWorld2Screen transform matrix.
|
* Transform according to the rotation from m_currentWorld2Screen transform matrix.
|
||||||
*
|
*
|
||||||
|
@ -452,6 +455,8 @@ public:
|
||||||
/// @copydoc GAL::EndDrawing()
|
/// @copydoc GAL::EndDrawing()
|
||||||
void EndDrawing() override;
|
void EndDrawing() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
/// Prepare Cairo surfaces for drawing
|
/// Prepare Cairo surfaces for drawing
|
||||||
void initSurface();
|
void initSurface();
|
||||||
|
|
||||||
|
@ -492,6 +497,9 @@ public:
|
||||||
///< Cairo-specific update handlers
|
///< Cairo-specific update handlers
|
||||||
bool updatedGalDisplayOptions( const GAL_DISPLAY_OPTIONS& aOptions ) override;
|
bool updatedGalDisplayOptions( const GAL_DISPLAY_OPTIONS& aOptions ) override;
|
||||||
|
|
||||||
|
///< For HiDPI support
|
||||||
|
double getScalingFactor() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Compositor related variables
|
// Compositor related variables
|
||||||
std::shared_ptr<CAIRO_COMPOSITOR> m_compositor; ///< Object for layers compositing
|
std::shared_ptr<CAIRO_COMPOSITOR> m_compositor; ///< Object for layers compositing
|
||||||
|
@ -507,7 +515,7 @@ protected:
|
||||||
wxEvtHandler* m_mouseListener; ///< Mouse listener
|
wxEvtHandler* m_mouseListener; ///< Mouse listener
|
||||||
wxEvtHandler* m_paintListener; ///< Paint listener
|
wxEvtHandler* m_paintListener; ///< Paint listener
|
||||||
unsigned int m_bufferSize; ///< Size of buffers cairoOutput, bitmapBuffers
|
unsigned int m_bufferSize; ///< Size of buffers cairoOutput, bitmapBuffers
|
||||||
unsigned char* m_wxOutput; ///< wxImage compatible buffer
|
wxBitmap* m_wxBitmap; ///< Output buffer bitmap
|
||||||
|
|
||||||
// Variables related to Cairo <-> wxWidgets
|
// Variables related to Cairo <-> wxWidgets
|
||||||
unsigned char* m_bitmapBuffer; ///< Storage of the Cairo image
|
unsigned char* m_bitmapBuffer; ///< Storage of the Cairo image
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* This program source code file is part of KICAD, a free EDA CAD application.
|
* This program source code file is part of KICAD, a free EDA CAD application.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
* Copyright (C) 2012 Torsten Hueter, torstenhtr <at> gmx.de
|
||||||
* Copyright (C) 2016-2021 KiCad Developers, see AUTHORS.txt for contributors.
|
* Copyright (C) 2016-2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||||
*
|
*
|
||||||
* Graphics Abstraction Layer (GAL) - base class
|
* Graphics Abstraction Layer (GAL) - base class
|
||||||
*
|
*
|
||||||
|
@ -1038,7 +1038,8 @@ protected:
|
||||||
UTIL::LINK m_observerLink;
|
UTIL::LINK m_observerLink;
|
||||||
|
|
||||||
std::stack<double> m_depthStack; ///< Stored depth values
|
std::stack<double> m_depthStack; ///< Stored depth values
|
||||||
VECTOR2I m_screenSize; ///< Screen size in screen coordinates
|
VECTOR2I m_screenSize; ///< Screen size in screen (wx logical) coordinates
|
||||||
|
VECTOR2I m_bitmapSize; ///< Bitmap size, in physical pixels
|
||||||
|
|
||||||
double m_worldUnitLength; ///< The unit length of the world coordinates [inch]
|
double m_worldUnitLength; ///< The unit length of the world coordinates [inch]
|
||||||
double m_screenDPI; ///< The dots per inch of the screen
|
double m_screenDPI; ///< The dots per inch of the screen
|
||||||
|
|
Loading…
Reference in New Issue