mirror of https://github.com/adamdruppe/arsd.git
Merge pull request #409 from analogjupiter/pixelpresenter
Add Pixmap Presenter module
This commit is contained in:
commit
f017816d05
|
@ -22,6 +22,8 @@ Future release, likely May 2024 or later.
|
||||||
|
|
||||||
Nothing is planned for it at this time.
|
Nothing is planned for it at this time.
|
||||||
|
|
||||||
|
arsd.pixmappresenter was added.
|
||||||
|
|
||||||
## 11.0
|
## 11.0
|
||||||
|
|
||||||
Released: Planned for May 2023, actually out August 2023.
|
Released: Planned for May 2023, actually out August 2023.
|
||||||
|
|
13
dub.json
13
dub.json
|
@ -681,6 +681,19 @@
|
||||||
"arsd-official:color_base":"*"
|
"arsd-official:color_base":"*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pixmappresenter",
|
||||||
|
"description": "High-level display library. Designed to blit fully-rendered frames to the screen.",
|
||||||
|
"targetType": "library",
|
||||||
|
"sourceFiles": ["pixmappresenter.d"],
|
||||||
|
"dependencies": {
|
||||||
|
"arsd-official:color_base":"*",
|
||||||
|
"arsd-official:simpledisplay":"*"
|
||||||
|
},
|
||||||
|
"dflags-dmd": ["-mv=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"],
|
||||||
|
"dflags-ldc": ["--mv=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"],
|
||||||
|
"dflags-gdc": ["-fmodule-file=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ttf",
|
"name": "ttf",
|
||||||
"description": "port of stb_ttf to D",
|
"description": "port of stb_ttf to D",
|
||||||
|
|
|
@ -86,6 +86,8 @@
|
||||||
$(H4 $(ID desktop-game) Games)
|
$(H4 $(ID desktop-game) Games)
|
||||||
See [arsd.simpledisplay] and [arsd.gamehelpers].
|
See [arsd.simpledisplay] and [arsd.gamehelpers].
|
||||||
|
|
||||||
|
Check out [arsd.pixmappresenter] for old-skool games that blit fully-rendered frames to the screen.
|
||||||
|
|
||||||
$(H4 $(ID desktop-gui) GUIs)
|
$(H4 $(ID desktop-gui) GUIs)
|
||||||
See [arsd.minigui], [arsd.nanovega], and also: https://github.com/drug007/nanogui
|
See [arsd.minigui], [arsd.nanovega], and also: https://github.com/drug007/nanogui
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,955 @@
|
||||||
|
/+
|
||||||
|
== pixmappresenter ==
|
||||||
|
Copyright Elias Batek (0xEAB) 2023.
|
||||||
|
Distributed under the Boost Software License, Version 1.0.
|
||||||
|
+/
|
||||||
|
/++
|
||||||
|
$(B Pixmap Presenter) is a high-level display library for one specific scenario:
|
||||||
|
Blitting fully-rendered frames to the screen.
|
||||||
|
|
||||||
|
This is useful for software-rendered applications.
|
||||||
|
Think of old-skool games, emulators etc.
|
||||||
|
|
||||||
|
This library builds upon [arsd.simpledisplay] and [arsd.color].
|
||||||
|
It wraps a [arsd.simpledisplay.SimpleWindow|SimpleWindow]) and displays the provided frame data.
|
||||||
|
Each frame is automatically centered on, and optionally scaled to, the carrier window.
|
||||||
|
This processing is done with hardware acceleration (OpenGL).
|
||||||
|
Later versions might add a software-mode.
|
||||||
|
|
||||||
|
Several $(B scaling) modes are supported.
|
||||||
|
Most notably `keepAspectRatio` that scales frames to the while preserving the original aspect ratio.
|
||||||
|
See [Scaling] for details.
|
||||||
|
|
||||||
|
$(PITFALL
|
||||||
|
This module is $(B work in progress).
|
||||||
|
API is subject to changes until further notice.
|
||||||
|
)
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
This example displays a blue frame that increases in color intensity,
|
||||||
|
then jumps back to black and the process repeats.
|
||||||
|
|
||||||
|
---
|
||||||
|
void main() {
|
||||||
|
// Internal resolution of the images (“frames”) we will render.
|
||||||
|
// From the PixmapPresenter’s perspective,
|
||||||
|
// these are the “fully-rendered frames” that it will blit to screen.
|
||||||
|
// They may be up- & down-scaled to the window’s actual size
|
||||||
|
// (according to the chosen scaling mode) by the presenter.
|
||||||
|
const resolution = Size(240, 120);
|
||||||
|
|
||||||
|
// Let’s create a new presenter.
|
||||||
|
// (For more fine-grained control there’s also a constructor overload that
|
||||||
|
// accepts a [PresenterConfig] instance).
|
||||||
|
auto presenter = new PixmapPresenter(
|
||||||
|
"Demo", // window title
|
||||||
|
resolution, // internal resolution
|
||||||
|
Size(960, 480), // initial window size (optional; default: =resolution)
|
||||||
|
);
|
||||||
|
|
||||||
|
// This variable will be “shared” across events (and frames).
|
||||||
|
int blueChannel = 0;
|
||||||
|
|
||||||
|
// Run the eventloop.
|
||||||
|
// The callback delegate will get executed every ~16ms (≙ ~60FPS) and schedule a redraw.
|
||||||
|
presenter.eventLoop(16, delegate() {
|
||||||
|
// Update the frame(buffer) here…
|
||||||
|
|
||||||
|
// Construct an RGB color value.
|
||||||
|
auto color = Pixel(0x00, 0x00, blueChannel);
|
||||||
|
// For demo purposes, apply it to the whole framebuffer.
|
||||||
|
presenter.framebuffer.clear(color);
|
||||||
|
|
||||||
|
// Increment the amount of blue to be used by the next frame.
|
||||||
|
++blueChannel;
|
||||||
|
// reset if greater than 0xFF (=ubyte.max)
|
||||||
|
if (blueChannel > 0xFF)
|
||||||
|
blueChannel = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minimal example
|
||||||
|
|
||||||
|
---
|
||||||
|
void main() {
|
||||||
|
auto pmp = new PixmapPresenter("My Pixmap App", Size(640, 480));
|
||||||
|
pmp.framebuffer.clear(rgb(0xFF, 0x00, 0x99));
|
||||||
|
pmp.eventLoop();
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
### Advanced example
|
||||||
|
|
||||||
|
---
|
||||||
|
import arsd.pixmappresenter;
|
||||||
|
import arsd.simpledisplay : MouseEvent;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Internal resolution of the images (“frames”) we will render.
|
||||||
|
// For further details, check out the previous example.
|
||||||
|
const resolution = Size(240, 120);
|
||||||
|
|
||||||
|
// Configure our presenter in advance.
|
||||||
|
auto cfg = PresenterConfig();
|
||||||
|
cfg.window.title = "Demo II";
|
||||||
|
cfg.window.size = Size(960, 480);
|
||||||
|
cfg.renderer.resolution = resolution;
|
||||||
|
cfg.renderer.scaling = Scaling.integer; // integer scaling
|
||||||
|
// → The frame on-screen will
|
||||||
|
// always have a size that is a
|
||||||
|
// multiple of the internal
|
||||||
|
// resolution.
|
||||||
|
// The gentle reader might have noticed that the integer scaling will result
|
||||||
|
// in a padding/border area around the image for most window sizes.
|
||||||
|
// How about changing its color?
|
||||||
|
cfg.renderer.background = ColorF(Pixel.white);
|
||||||
|
|
||||||
|
// Let’s instantiate a new presenter with the previously created config.
|
||||||
|
auto presenter = new PixmapPresenter(cfg);
|
||||||
|
|
||||||
|
// Start with a green frame, so we can easily observe what’s going on.
|
||||||
|
presenter.framebuffer.clear(rgb(0x00, 0xDD, 0x00));
|
||||||
|
|
||||||
|
int line = 0;
|
||||||
|
ubyte color = 0;
|
||||||
|
byte colorDelta = 2;
|
||||||
|
|
||||||
|
// Run the eventloop.
|
||||||
|
// Note how the callback delegate returns a [LoopCtrl] instance.
|
||||||
|
return presenter.eventLoop(delegate() {
|
||||||
|
// Determine the start and end index of the current line in the
|
||||||
|
// framebuffer.
|
||||||
|
immutable x0 = line * resolution.width;
|
||||||
|
immutable x1 = x0 + resolution.width;
|
||||||
|
|
||||||
|
// Change the color of the current line
|
||||||
|
presenter.framebuffer.data[x0 .. x1] = rgb(color, color, 0xFF);
|
||||||
|
|
||||||
|
// Determine the color to use for the next line
|
||||||
|
// (to be applied on the next update).
|
||||||
|
color += colorDelta;
|
||||||
|
if (color == 0x00)
|
||||||
|
colorDelta = 2;
|
||||||
|
else if (color >= 0xFE)
|
||||||
|
colorDelta = -2;
|
||||||
|
|
||||||
|
// Increment the line counter; reset to 0 once we’ve reached the
|
||||||
|
// end of the framebuffer (=the final/last line).
|
||||||
|
++line;
|
||||||
|
if (line == resolution.height)
|
||||||
|
line = 0;
|
||||||
|
|
||||||
|
// Schedule a redraw in ~16ms.
|
||||||
|
return LoopCtrl.redrawIn(16);
|
||||||
|
}, delegate(MouseEvent ev) {
|
||||||
|
// toggle fullscreen mode on double-click
|
||||||
|
if (ev.doubleClick) {
|
||||||
|
presenter.isFullscreen = !presenter.isFullscreen;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
---
|
||||||
|
+/
|
||||||
|
module arsd.pixmappresenter;
|
||||||
|
|
||||||
|
import arsd.color;
|
||||||
|
import arsd.simpledisplay;
|
||||||
|
|
||||||
|
/*
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Complete documentation
|
||||||
|
- Additional renderer implementations:
|
||||||
|
- a `ScreenPainter`-based renderer
|
||||||
|
- a legacy OpenGL renderer (maybe)
|
||||||
|
- Is there something in arsd that serves a similar purpose to `PixelBuffer`?
|
||||||
|
- Minimum window size
|
||||||
|
- or something similar
|
||||||
|
- to ensure `Scaling.integer` doesn’t break “unexpectedly”
|
||||||
|
*/
|
||||||
|
|
||||||
|
///
|
||||||
|
alias Pixel = Color;
|
||||||
|
|
||||||
|
///
|
||||||
|
alias ColorF = arsd.color.ColorF;
|
||||||
|
|
||||||
|
///
|
||||||
|
alias Size = arsd.color.Size;
|
||||||
|
|
||||||
|
///
|
||||||
|
alias Point = arsd.color.Point;
|
||||||
|
|
||||||
|
// verify assumption(s)
|
||||||
|
static assert(Pixel.sizeof == uint.sizeof);
|
||||||
|
|
||||||
|
// is the Timer class available on this platform?
|
||||||
|
private enum hasTimer = is(Timer == class);
|
||||||
|
|
||||||
|
/// casts value `v` to type `T`
|
||||||
|
auto ref T typeCast(T, S)(auto ref S v) {
|
||||||
|
return cast(T) v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@safe pure nothrow @nogc {
|
||||||
|
///
|
||||||
|
Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) {
|
||||||
|
return Pixel(r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
Pixel rgb(ubyte r, ubyte g, ubyte b) {
|
||||||
|
return rgba(r, g, b, 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Pixel data container
|
||||||
|
+/
|
||||||
|
struct PixelBuffer {
|
||||||
|
|
||||||
|
/// Pixel data
|
||||||
|
Pixel[] data;
|
||||||
|
|
||||||
|
/// Pixel per row
|
||||||
|
int width;
|
||||||
|
|
||||||
|
@safe pure nothrow:
|
||||||
|
|
||||||
|
this(Size size) {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// undocumented: really shouldn’t be used.
|
||||||
|
// carries the risks of `length` and `width` getting out of sync accidentally.
|
||||||
|
deprecated("Use `size` instead.")
|
||||||
|
void length(int value) {
|
||||||
|
data.length = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Changes the size of the buffer
|
||||||
|
|
||||||
|
Reallocates the underlying pixel array.
|
||||||
|
+/
|
||||||
|
void size(Size value) {
|
||||||
|
data.length = value.area;
|
||||||
|
width = value.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
void size(int totalPixels, int width)
|
||||||
|
in (length % width == 0) {
|
||||||
|
data.length = totalPixels;
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@safe pure nothrow @nogc:
|
||||||
|
|
||||||
|
/// Height of the buffer, i.e. the number of lines
|
||||||
|
int height() inout {
|
||||||
|
if (data.length == 0)
|
||||||
|
return 0;
|
||||||
|
return (cast(int) data.length / width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rectangular size of the buffer
|
||||||
|
Size size() inout {
|
||||||
|
return Size(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Length of the buffer, i.e. the number of pixels
|
||||||
|
int length() inout {
|
||||||
|
return cast(int) data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Number of bytes per line
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
width × Pixel.sizeof
|
||||||
|
+/
|
||||||
|
int pitch() inout {
|
||||||
|
return (width * int(Pixel.sizeof));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the buffer’s contents (by setting each pixel to the same color)
|
||||||
|
void clear(Pixel value) {
|
||||||
|
data[] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @safe pure nothrow @nogc {
|
||||||
|
|
||||||
|
// keep aspect ratio (contain)
|
||||||
|
bool karContainNeedsDownscaling(const Size drawing, const Size canvas) {
|
||||||
|
return (drawing.width > canvas.width)
|
||||||
|
|| (drawing.height > canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep aspect ratio (contain)
|
||||||
|
int karContainScalingFactorInt(const Size drawing, const Size canvas) {
|
||||||
|
const int w = canvas.width / drawing.width;
|
||||||
|
const int h = canvas.height / drawing.height;
|
||||||
|
|
||||||
|
return (w < h) ? w : h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep aspect ratio (contain; FP variant)
|
||||||
|
float karContainScalingFactorF(const Size drawing, const Size canvas) {
|
||||||
|
const w = float(canvas.width) / float(drawing.width);
|
||||||
|
const h = float(canvas.height) / float(drawing.height);
|
||||||
|
|
||||||
|
return (w < h) ? w : h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep aspect ratio (cover)
|
||||||
|
float karCoverScalingFactorF(const Size drawing, const Size canvas) {
|
||||||
|
const w = float(canvas.width) / float(drawing.width);
|
||||||
|
const h = float(canvas.height) / float(drawing.height);
|
||||||
|
|
||||||
|
return (w > h) ? w : h;
|
||||||
|
}
|
||||||
|
|
||||||
|
Size deltaPerimeter(const Size a, const Size b) {
|
||||||
|
return Size(
|
||||||
|
a.width - b.width,
|
||||||
|
a.height - b.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Point offsetCenter(const Size drawing, const Size canvas) {
|
||||||
|
auto delta = canvas.deltaPerimeter(drawing);
|
||||||
|
return (cast(Point) delta) >> 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Scaling/Fit Modes
|
||||||
|
|
||||||
|
Each scaling modes has unique behavior for different window-size to frame-size ratios.
|
||||||
|
|
||||||
|
Unfortunately, there are no universally applicable naming conventions for these modes.
|
||||||
|
In fact, different implementations tend to contradict each other.
|
||||||
|
|
||||||
|
$(SMALL_TABLE
|
||||||
|
Mode feature matrix
|
||||||
|
Mode | Aspect Ratio | Pixel Ratio | Cropping | Border | Comment(s)
|
||||||
|
`none` | preserved | preserved | yes | 4 | Crops if the `window.size < frame.size`.
|
||||||
|
`stretch` | no | no | no | none |
|
||||||
|
`contain` | preserved | no | no | 2 | Letterboxing/Pillarboxing
|
||||||
|
`integer` | preserved | preserved | no | 4 | Works only if `window.size >= frame.size`.
|
||||||
|
`integerFP` | preserved | when up | no | 4 or 2 | Hybrid: int upscaling, floating-point downscaling
|
||||||
|
`cover` | preserved | no | yes | none |
|
||||||
|
)
|
||||||
|
|
||||||
|
$(SMALL_TABLE
|
||||||
|
Feature | Definition
|
||||||
|
Aspect Ratio | Whether the original aspect ratio (width ÷ height) of the input frame is preserved
|
||||||
|
Pixel Ratio | Whether the orignal pixel ratio (= square) is preserved
|
||||||
|
Cropping | Whether the outer areas of the input frame might get cut off
|
||||||
|
Border | The number of padding-areas/borders that can potentially appear around the frame
|
||||||
|
)
|
||||||
|
|
||||||
|
For your convience, aliases matching the [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
|
||||||
|
CSS property are provided, too. These are prefixed with `css`.
|
||||||
|
Currently there is no equivalent for `scale-down` as it does not appear to be particularly useful here.
|
||||||
|
+/
|
||||||
|
enum Scaling {
|
||||||
|
none = 0, ///
|
||||||
|
stretch, ///
|
||||||
|
contain, ///
|
||||||
|
integer, ///
|
||||||
|
integerFP, ///
|
||||||
|
cover, ///
|
||||||
|
|
||||||
|
// aliases
|
||||||
|
center = none, ///
|
||||||
|
keepAspectRatio = contain, ///
|
||||||
|
|
||||||
|
// CSS `object-fit` style aliases
|
||||||
|
cssNone = none, /// equivalent CSS: `object-fit: none;`
|
||||||
|
cssContain = contain, /// equivalent CSS: `object-fit: contain;`
|
||||||
|
cssFill = stretch, /// equivalent CSS: `object-fit: fill;`
|
||||||
|
cssCover = cover, /// equivalent CSS: `object-fit: cover;`
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
enum ScalingFilter {
|
||||||
|
nearest, /// nearest neighbor → blocky/pixel’ish
|
||||||
|
linear, /// (bi-)linear interpolation → smooth/blurry
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
struct PresenterConfig {
|
||||||
|
Window window; ///
|
||||||
|
Renderer renderer; ///
|
||||||
|
|
||||||
|
///
|
||||||
|
static struct Renderer {
|
||||||
|
/++
|
||||||
|
Internal resolution
|
||||||
|
+/
|
||||||
|
Size resolution;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Scaling method
|
||||||
|
to apply when `window.size` != `resolution`
|
||||||
|
+/
|
||||||
|
Scaling scaling = Scaling.keepAspectRatio;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Filter
|
||||||
|
+/
|
||||||
|
ScalingFilter filter = ScalingFilter.nearest;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Background color
|
||||||
|
+/
|
||||||
|
ColorF background = ColorF(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
///
|
||||||
|
void setPixelPerfect() {
|
||||||
|
scaling = Scaling.integer;
|
||||||
|
filter = ScalingFilter.nearest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
static struct Window {
|
||||||
|
string title = "ARSD Pixmap Presenter";
|
||||||
|
Size size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// undocumented
|
||||||
|
struct PresenterObjects {
|
||||||
|
PixelBuffer framebuffer;
|
||||||
|
SimpleWindow window;
|
||||||
|
PresenterConfig config;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
struct WantsOpenGl {
|
||||||
|
bool wanted; /// Is OpenGL wanted?
|
||||||
|
ubyte vMaj; /// major version
|
||||||
|
ubyte vMin; /// minor version
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
interface PixelRenderer {
|
||||||
|
/++
|
||||||
|
Does this renderer use OpenGL?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether the renderer requires an OpenGL-enabled window
|
||||||
|
and which version is expected.
|
||||||
|
+/
|
||||||
|
public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Setup function
|
||||||
|
|
||||||
|
Called once during setup.
|
||||||
|
Perform initialization tasks in here.
|
||||||
|
|
||||||
|
$(NOTE
|
||||||
|
The final thing a setup function does
|
||||||
|
is usually to call `reconfigure()` on the renderer.
|
||||||
|
)
|
||||||
|
|
||||||
|
Params:
|
||||||
|
pro = Pointer to the [PresenterObjects] of the presenter. To be stored for later use.
|
||||||
|
+/
|
||||||
|
public void setup(PresenterObjects* pro);
|
||||||
|
|
||||||
|
/++
|
||||||
|
Reconfigures the renderer
|
||||||
|
|
||||||
|
Called upon configuration changes.
|
||||||
|
The new config can be found in the [PresenterObjects] received during `setup()`.
|
||||||
|
+/
|
||||||
|
public void reconfigure();
|
||||||
|
|
||||||
|
/++
|
||||||
|
Schedules a redraw
|
||||||
|
+/
|
||||||
|
public void redrawSchedule();
|
||||||
|
|
||||||
|
/++
|
||||||
|
Triggers a redraw
|
||||||
|
+/
|
||||||
|
public void redrawNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
final class OpenGL3PixelRenderer : PixelRenderer {
|
||||||
|
|
||||||
|
private {
|
||||||
|
PresenterObjects* _pro;
|
||||||
|
|
||||||
|
bool _clear = true;
|
||||||
|
|
||||||
|
GLfloat[16] _vertices;
|
||||||
|
OpenGlShader _shader;
|
||||||
|
GLuint _vao;
|
||||||
|
GLuint _vbo;
|
||||||
|
GLuint _ebo;
|
||||||
|
GLuint _texture = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
public this() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc {
|
||||||
|
return WantsOpenGl(true, 3, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make this ctor?
|
||||||
|
public void setup(PresenterObjects* pro) {
|
||||||
|
_pro = pro;
|
||||||
|
_pro.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
|
||||||
|
_pro.window.redrawOpenGlScene = &this.redrawOpenGlScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
private {
|
||||||
|
void visibleForTheFirstTime() {
|
||||||
|
_pro.window.setAsCurrentOpenGlContext();
|
||||||
|
gl3.loadDynamicLibrary();
|
||||||
|
|
||||||
|
this.compileLinkShader();
|
||||||
|
this.setupVertexObjects();
|
||||||
|
|
||||||
|
this.reconfigure();
|
||||||
|
}
|
||||||
|
|
||||||
|
void redrawOpenGlScene() {
|
||||||
|
if (_clear) {
|
||||||
|
glClearColor(
|
||||||
|
_pro.config.renderer.background.r,
|
||||||
|
_pro.config.renderer.background.g,
|
||||||
|
_pro.config.renderer.background.b,
|
||||||
|
_pro.config.renderer.background.a
|
||||||
|
);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
_clear = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, _texture);
|
||||||
|
glTexSubImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0, 0,
|
||||||
|
_pro.config.renderer.resolution.width, _pro.config.renderer.resolution.height,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
cast(void*) _pro.framebuffer.data.ptr
|
||||||
|
);
|
||||||
|
|
||||||
|
glUseProgram(_shader.shaderProgram);
|
||||||
|
glBindVertexArray(_vao);
|
||||||
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private {
|
||||||
|
void compileLinkShader() {
|
||||||
|
_shader = new OpenGlShader(
|
||||||
|
OpenGlShader.Source(GL_VERTEX_SHADER, `
|
||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec2 aPos;
|
||||||
|
layout (location = 1) in vec2 aTexCoord;
|
||||||
|
|
||||||
|
out vec2 TexCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
|
||||||
|
TexCoord = aTexCoord;
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
OpenGlShader.Source(GL_FRAGMENT_SHADER, `
|
||||||
|
#version 330 core
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
in vec2 TexCoord;
|
||||||
|
|
||||||
|
uniform sampler2D sampler;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
FragColor = texture(sampler, TexCoord);
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupVertexObjects() {
|
||||||
|
glGenVertexArrays(1, &_vao);
|
||||||
|
glBindVertexArray(_vao);
|
||||||
|
|
||||||
|
glGenBuffers(1, &_vbo);
|
||||||
|
glGenBuffers(1, &_ebo);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo);
|
||||||
|
glBufferDataSlice(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
||||||
|
glBufferDataSlice(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, null);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, cast(void*)(2 * GLfloat.sizeof));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupTexture() {
|
||||||
|
if (_texture == 0) {
|
||||||
|
glGenTextures(1, &_texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, _texture);
|
||||||
|
|
||||||
|
final switch (_pro.config.renderer.filter) with (ScalingFilter) {
|
||||||
|
case nearest:
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
break;
|
||||||
|
case linear:
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
_pro.config.renderer.resolution.width, _pro.config.renderer.resolution.height,
|
||||||
|
0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reconfigure() {
|
||||||
|
Size viewport;
|
||||||
|
|
||||||
|
final switch (_pro.config.renderer.scaling) {
|
||||||
|
|
||||||
|
case Scaling.none:
|
||||||
|
viewport = _pro.config.renderer.resolution;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Scaling.stretch:
|
||||||
|
viewport = _pro.config.window.size;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Scaling.contain:
|
||||||
|
const float scaleF = karContainScalingFactorF(_pro.config.renderer.resolution, _pro.config.window.size);
|
||||||
|
viewport = Size(
|
||||||
|
typeCast!int(scaleF * _pro.config.renderer.resolution.width),
|
||||||
|
typeCast!int(scaleF * _pro.config.renderer.resolution.height),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Scaling.integer:
|
||||||
|
const int scaleI = karContainScalingFactorInt(_pro.config.renderer.resolution, _pro.config.window.size);
|
||||||
|
viewport = (_pro.config.renderer.resolution * scaleI);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Scaling.integerFP:
|
||||||
|
if (karContainNeedsDownscaling(_pro.config.renderer.resolution, _pro.config.window.size)) {
|
||||||
|
goto case Scaling.contain;
|
||||||
|
}
|
||||||
|
goto case Scaling.integer;
|
||||||
|
|
||||||
|
case Scaling.cover:
|
||||||
|
const float fillF = karCoverScalingFactorF(_pro.config.renderer.resolution, _pro.config.window.size);
|
||||||
|
viewport = Size(
|
||||||
|
typeCast!int(fillF * _pro.config.renderer.resolution.width),
|
||||||
|
typeCast!int(fillF * _pro.config.renderer.resolution.height),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Point viewportPos = offsetCenter(viewport, _pro.config.window.size);
|
||||||
|
glViewport(viewportPos.x, viewportPos.y, viewport.width, viewport.height);
|
||||||
|
this.setupTexture();
|
||||||
|
_clear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void redrawSchedule() {
|
||||||
|
_pro.window.redrawOpenGlSceneSoon();
|
||||||
|
}
|
||||||
|
|
||||||
|
void redrawNow() {
|
||||||
|
_pro.window.redrawOpenGlSceneNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private {
|
||||||
|
static immutable GLfloat[] vertices = [
|
||||||
|
//dfmt off
|
||||||
|
// positions // texture coordinates
|
||||||
|
1.0f, 1.0f, 1.0f, 0.0f,
|
||||||
|
1.0f, -1.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f,
|
||||||
|
//dfmt on
|
||||||
|
];
|
||||||
|
|
||||||
|
static immutable GLuint[] indices = [
|
||||||
|
//dfmt off
|
||||||
|
0, 1, 3,
|
||||||
|
1, 2, 3,
|
||||||
|
//dfmt on
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
struct LoopCtrl {
|
||||||
|
int interval; /// in milliseconds
|
||||||
|
bool redraw; ///
|
||||||
|
|
||||||
|
///
|
||||||
|
@disable this();
|
||||||
|
|
||||||
|
@safe pure nothrow @nogc:
|
||||||
|
|
||||||
|
private this(int interval, bool redraw) {
|
||||||
|
this.interval = interval;
|
||||||
|
this.redraw = redraw;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
static LoopCtrl waitFor(int intervalMS) {
|
||||||
|
return LoopCtrl(intervalMS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
static LoopCtrl redrawIn(int intervalMS) {
|
||||||
|
return LoopCtrl(intervalMS, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+/
|
||||||
|
final class PixmapPresenter {
|
||||||
|
|
||||||
|
private {
|
||||||
|
PresenterObjects* _pro;
|
||||||
|
PixelRenderer _renderer;
|
||||||
|
|
||||||
|
static if (hasTimer) {
|
||||||
|
Timer _timer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctors
|
||||||
|
public {
|
||||||
|
|
||||||
|
///
|
||||||
|
this(const PresenterConfig config, bool useOpenGl = true) {
|
||||||
|
if (useOpenGl) {
|
||||||
|
this(config, new OpenGL3PixelRenderer());
|
||||||
|
} else {
|
||||||
|
assert(false, "Not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
this(const PresenterConfig config, PixelRenderer renderer) {
|
||||||
|
_renderer = renderer;
|
||||||
|
|
||||||
|
// create software framebuffer
|
||||||
|
auto framebuffer = PixelBuffer(config.renderer.resolution);
|
||||||
|
|
||||||
|
// OpenGL?
|
||||||
|
auto openGlOptions = OpenGlOptions.no;
|
||||||
|
const openGl = _renderer.wantsOpenGl;
|
||||||
|
if (openGl.wanted) {
|
||||||
|
setOpenGLContextVersion(openGl.vMaj, openGl.vMin);
|
||||||
|
openGLContextCompatible = false;
|
||||||
|
|
||||||
|
openGlOptions = OpenGlOptions.yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// spawn window
|
||||||
|
auto window = new SimpleWindow(
|
||||||
|
config.window.size,
|
||||||
|
config.window.title,
|
||||||
|
openGlOptions,
|
||||||
|
Resizability.allowResizing,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.windowResized = &this.windowResized;
|
||||||
|
|
||||||
|
// alloc objects
|
||||||
|
_pro = new PresenterObjects(
|
||||||
|
framebuffer,
|
||||||
|
window,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
_renderer.setup(_pro);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional convience ctors
|
||||||
|
public {
|
||||||
|
|
||||||
|
///
|
||||||
|
this(
|
||||||
|
string title,
|
||||||
|
const Size resolution,
|
||||||
|
const Size initialWindowSize,
|
||||||
|
Scaling scaling = Scaling.contain,
|
||||||
|
ScalingFilter filter = ScalingFilter.nearest,
|
||||||
|
) {
|
||||||
|
auto cfg = PresenterConfig();
|
||||||
|
|
||||||
|
cfg.window.title = title;
|
||||||
|
cfg.renderer.resolution = resolution;
|
||||||
|
cfg.window.size = initialWindowSize;
|
||||||
|
cfg.renderer.scaling = scaling;
|
||||||
|
cfg.renderer.filter = filter;
|
||||||
|
|
||||||
|
this(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
this(
|
||||||
|
string title,
|
||||||
|
const Size resolution,
|
||||||
|
Scaling scaling = Scaling.contain,
|
||||||
|
ScalingFilter filter = ScalingFilter.nearest,
|
||||||
|
) {
|
||||||
|
this(title, resolution, resolution, scaling, filter,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public functions
|
||||||
|
public {
|
||||||
|
|
||||||
|
/++
|
||||||
|
Runs the event loop (with a pulse timer)
|
||||||
|
|
||||||
|
A redraw will be scheduled automatically each pulse.
|
||||||
|
+/
|
||||||
|
int eventLoop(T...)(long pulseTimeout, void delegate() onPulse, T eventHandlers) {
|
||||||
|
// run event-loop with pulse timer
|
||||||
|
return _pro.window.eventLoop(
|
||||||
|
pulseTimeout,
|
||||||
|
delegate() { onPulse(); this.scheduleRedraw(); },
|
||||||
|
eventHandlers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//dfmt off
|
||||||
|
/++
|
||||||
|
Runs the event loop
|
||||||
|
|
||||||
|
Redraws have to manually scheduled through [scheduleRedraw] when using this overload.
|
||||||
|
+/
|
||||||
|
int eventLoop(T...)(T eventHandlers) if (
|
||||||
|
(T.length == 0) || (is(T[0] == delegate) && !is(typeof(() { return T[0](); }()) == LoopCtrl))
|
||||||
|
) {
|
||||||
|
return _pro.window.eventLoop(eventHandlers);
|
||||||
|
}
|
||||||
|
//dfmt on
|
||||||
|
|
||||||
|
static if (hasTimer) {
|
||||||
|
/++
|
||||||
|
Runs the event loop
|
||||||
|
with [LoopCtrl] timing mechanism
|
||||||
|
+/
|
||||||
|
int eventLoop(T...)(LoopCtrl delegate() callback, T eventHandlers) {
|
||||||
|
if (callback !is null) {
|
||||||
|
LoopCtrl prev = LoopCtrl(1, true);
|
||||||
|
|
||||||
|
_timer = new Timer(prev.interval, delegate() {
|
||||||
|
// redraw if requested by previous ctrl message
|
||||||
|
if (prev.redraw) {
|
||||||
|
_renderer.redrawNow();
|
||||||
|
prev.redraw = false; // done
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute callback
|
||||||
|
const LoopCtrl ctrl = callback();
|
||||||
|
|
||||||
|
// different than previous ctrl message?
|
||||||
|
if (ctrl.interval != prev.interval) {
|
||||||
|
// update timer
|
||||||
|
_timer.changeTime(ctrl.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// save ctrl message
|
||||||
|
prev = ctrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// run event-loop
|
||||||
|
return _pro.window.eventLoop(0, eventHandlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
PixelBuffer framebuffer() @safe pure nothrow @nogc {
|
||||||
|
return _pro.framebuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void reconfigure(const PresenterConfig config) {
|
||||||
|
assert(false, "Not implemented");
|
||||||
|
//_framebuffer.size = config.internalResolution;
|
||||||
|
//_renderer.reconfigure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void scheduleRedraw() {
|
||||||
|
_renderer.redrawSchedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
bool isFullscreen() {
|
||||||
|
return _pro.window.fullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
void isFullscreen(bool enabled) {
|
||||||
|
return _pro.window.fullscreen = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Returns the underlying `SimpleWindow`
|
||||||
|
|
||||||
|
$(WARNING
|
||||||
|
This is unsupported; use at your own risk.
|
||||||
|
|
||||||
|
Tinkering with the window directly can break all sort of things
|
||||||
|
that a presenter or renderer could possibly have set up.
|
||||||
|
)
|
||||||
|
+/
|
||||||
|
SimpleWindow tinker() @safe pure nothrow @nogc {
|
||||||
|
return _pro.window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// event handlers
|
||||||
|
private {
|
||||||
|
void windowResized(int width, int height) {
|
||||||
|
_pro.config.window.size = Size(width, height);
|
||||||
|
_renderer.reconfigure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5605,12 +5605,7 @@ class Timer {
|
||||||
|
|
||||||
mapping[fd] = this;
|
mapping[fd] = this;
|
||||||
|
|
||||||
itimerspec value;
|
itimerspec value = makeItimerspec(intervalInMilliseconds);
|
||||||
value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
|
|
||||||
value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
|
|
||||||
|
|
||||||
value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
|
|
||||||
value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
|
|
||||||
|
|
||||||
if(timerfd_settime(fd, 0, &value, null) == -1)
|
if(timerfd_settime(fd, 0, &value, null) == -1)
|
||||||
throw new Exception("couldn't make pulse timer");
|
throw new Exception("couldn't make pulse timer");
|
||||||
|
@ -5683,7 +5678,6 @@ class Timer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void changeTime(int intervalInMilliseconds)
|
void changeTime(int intervalInMilliseconds)
|
||||||
{
|
{
|
||||||
this.intervalInMilliseconds = intervalInMilliseconds;
|
this.intervalInMilliseconds = intervalInMilliseconds;
|
||||||
|
@ -5696,6 +5690,15 @@ class Timer {
|
||||||
if(handle is null || !SetWaitableTimer(handle, cast(LARGE_INTEGER*)&initialTime, intervalInMilliseconds, &timerCallback, handle, false))
|
if(handle is null || !SetWaitableTimer(handle, cast(LARGE_INTEGER*)&initialTime, intervalInMilliseconds, &timerCallback, handle, false))
|
||||||
throw new WindowsApiException("couldn't change pulse timer", GetLastError());
|
throw new WindowsApiException("couldn't change pulse timer", GetLastError());
|
||||||
}
|
}
|
||||||
|
} else version(linux) {
|
||||||
|
import core.sys.linux.timerfd;
|
||||||
|
|
||||||
|
itimerspec value = makeItimerspec(intervalInMilliseconds);
|
||||||
|
if(timerfd_settime(fd, 0, &value, null) == -1) {
|
||||||
|
throw new Exception("couldn't change pulse timer");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, "Timer.changeTime(int) is not implemented for this platform");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5706,6 +5709,21 @@ class Timer {
|
||||||
|
|
||||||
int lastEventLoopRoundTriggered;
|
int lastEventLoopRoundTriggered;
|
||||||
|
|
||||||
|
version(linux) {
|
||||||
|
static auto makeItimerspec(int intervalInMilliseconds) {
|
||||||
|
import core.sys.linux.timerfd;
|
||||||
|
|
||||||
|
itimerspec value;
|
||||||
|
value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
|
||||||
|
value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
|
||||||
|
|
||||||
|
value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
|
||||||
|
value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void trigger() {
|
void trigger() {
|
||||||
version(linux) {
|
version(linux) {
|
||||||
import unix = core.sys.posix.unistd;
|
import unix = core.sys.posix.unistd;
|
||||||
|
@ -18769,6 +18787,7 @@ extern(System) nothrow @nogc {
|
||||||
enum uint GL_RGB = 0x1907;
|
enum uint GL_RGB = 0x1907;
|
||||||
enum uint GL_BGRA = 0x80e1;
|
enum uint GL_BGRA = 0x80e1;
|
||||||
enum uint GL_RGBA = 0x1908;
|
enum uint GL_RGBA = 0x1908;
|
||||||
|
enum uint GL_RGBA8 = 0x8058;
|
||||||
enum uint GL_TEXTURE_2D = 0x0DE1;
|
enum uint GL_TEXTURE_2D = 0x0DE1;
|
||||||
enum uint GL_TEXTURE_MIN_FILTER = 0x2801;
|
enum uint GL_TEXTURE_MIN_FILTER = 0x2801;
|
||||||
enum uint GL_NEAREST = 0x2600;
|
enum uint GL_NEAREST = 0x2600;
|
||||||
|
|
Loading…
Reference in New Issue