1 module gbaid.render.renderer;
2 
3 import derelict.sdl2.sdl : SDL_ShowCursor, SDL_ENABLE, SDL_DISABLE;
4 
5 import gbaid.render.gl;
6 import gbaid.render.gl20;
7 import gbaid.render.shader;
8 
9 public class FrameRenderer {
10     private Context context = null;
11     private Program program = null;
12     private Texture texture = null;
13     private Program upscaleProgram = null;
14     private Texture upscaledTexture = null;
15     private FrameBuffer upscaleFrameBuffer = null;
16     private Texture outputTexture = null;
17     private VertexArray vertexArray = null;
18     private immutable uint frameWidth, frameHeight;
19     private int windowWidth, windowHeight;
20     private bool _fullScreen = false;
21     private bool _useVsync = false;
22     private FilteringMode filteringMode = FilteringMode.NONE;
23     private UpscalingMode upscalingMode = UpscalingMode.NONE;
24 
25     public this(uint frameWidth, uint frameHeight) {
26         this.frameWidth = frameWidth;
27         this.frameHeight = frameHeight;
28         windowWidth = frameWidth;
29         windowHeight = frameHeight;
30     }
31 
32     @property public void fullScreen(bool full) {
33         _fullScreen = full;
34 
35         if (context !is null) {
36             SDL_ShowCursor(full ? SDL_DISABLE : SDL_ENABLE);
37             context.setFullScreen(full);
38         }
39     }
40 
41     @property public void useVsync(bool use) {
42         _useVsync = use;
43 
44         if (context !is null) {
45             context.enableVsync(use);
46         }
47     }
48 
49     public void setScale(float scale) {
50         windowWidth = cast(uint) (frameWidth * scale + 0.5f);
51         windowHeight = cast(uint) (frameHeight * scale + 0.5f);
52 
53         if (context !is null) {
54             context.setWindowSize(windowWidth, windowHeight);
55         }
56     }
57 
58     public void setFilteringMode(FilteringMode mode) {
59         filteringMode = mode;
60     }
61 
62     public void setUpscalingMode(UpscalingMode mode) {
63         upscalingMode = mode;
64     }
65 
66     public void create() {
67         if (context !is null) {
68             return;
69         }
70 
71         scope (failure) {
72             destroy();
73         }
74 
75         context = new GL20Context();
76         context.setWindowTitle("GBAiD");
77         context.setResizable(true);
78         context.setWindowSize(windowWidth, windowHeight);
79         context.setFullScreen(_fullScreen);
80         context.enableVsync(_useVsync);
81         context.create();
82         context.enableCapability(CULL_FACE);
83 
84         SDL_ShowCursor(_fullScreen ? SDL_DISABLE : SDL_ENABLE);
85 
86         program = makeProgram(TEXTURE_POST_PROCESS_VERTEX_SHADER_SOURCE, WINDOW_OUTPUT_FRAGMENT_SHADER_SOURCE);
87         program.use();
88         program.bindSampler(0);
89 
90         texture = makeTexture(RGBA, RGB5_A1, frameWidth, frameHeight);
91 
92         final switch (upscalingMode) with (UpscalingMode) {
93             case NONE:
94                 upscaleProgram = null;
95                 upscaledTexture = null;
96                 upscaleFrameBuffer = null;
97                 break;
98             case EPX:
99                 upscaleProgram = makeProgram(TEXTURE_POST_PROCESS_VERTEX_SHADER_SOURCE, EPX_UPSCALE_FRAGMENT_SHADER_SOURCE);
100                 upscaledTexture = makeTexture(RGBA, RGBA8, frameWidth * 2, frameHeight * 2);
101                 break;
102             case XBR:
103                 upscaleProgram = makeProgram(TEXTURE_POST_PROCESS_VERTEX_SHADER_SOURCE, XBR_UPSCALE_FRAGMENT_SHADER_SOURCE);
104                 upscaledTexture = makeTexture(RGBA, RGBA8, frameWidth * 5, frameHeight * 5);
105                 break;
106             case BICUBIC:
107                 upscaleProgram = makeProgram(TEXTURE_POST_PROCESS_VERTEX_SHADER_SOURCE, BICUBIC_UPSCALE_FRAGMENT_SHADER_SOURCE);
108                 int actualWidth, actualHeight;
109                 context.getActualWindowSize(&actualWidth, &actualHeight);
110                 upscaledTexture = makeTexture(RGBA, RGBA8, actualWidth, actualHeight);
111                 texture.setWraps(CLAMP_TO_EDGE, CLAMP_TO_EDGE);
112                 break;
113         }
114         if (upscaleProgram !is null) {
115             upscaleFrameBuffer = context.newFrameBuffer();
116             upscaleFrameBuffer.create();
117             upscaleFrameBuffer.attach(COLOR_ATTACHMENT0, upscaledTexture);
118             if (!upscaleFrameBuffer.isComplete()) {
119                 throw new GLException("Upscale framebuffer is incomplete");
120             }
121 
122             upscaleProgram.use();
123             upscaleProgram.bindSampler(0);
124             upscaleProgram.setUniform("size", frameWidth, frameHeight);
125         }
126 
127         outputTexture = upscaleProgram !is null ? upscaledTexture : texture;
128         final switch (filteringMode) {
129             case FilteringMode.NONE:
130                 outputTexture.setFilters(NEAREST, NEAREST);
131                 break;
132             case FilteringMode.LINEAR:
133                 outputTexture.setFilters(LINEAR, LINEAR);
134                 break;
135         }
136 
137         vertexArray = context.newVertexArray();
138         vertexArray.create();
139         vertexArray.setData(generatePlane(2, 2));
140     }
141 
142     public void destroy() {
143         if (context is null) {
144             return;
145         }
146         context.destroy();
147         context = null;
148         program = null;
149         texture = null;
150         upscaleProgram = null;
151         upscaledTexture = null;
152         upscaleFrameBuffer = null;
153         outputTexture = null;
154         vertexArray = null;
155     }
156 
157     public void draw(short[] frame) {
158         if (!context.isCreated()) {
159             create();
160         }
161 
162         context.getWindowSize(&windowWidth, &windowHeight);
163         texture.setImageData(cast(ubyte[]) frame, frameWidth, frameHeight);
164         texture.bind(0);
165         if (upscaleProgram !is null) {
166             upscaleProgram.use();
167             upscaleFrameBuffer.bind();
168             context.setViewPort(0, 0, upscaledTexture.getWidth(), upscaledTexture.getHeight());
169             vertexArray.draw();
170             upscaleFrameBuffer.unbind();
171             upscaledTexture.bind(0);
172         }
173         program.use();
174         program.setUniform("inputSize", frameWidth, frameHeight);
175         program.setUniform("outputSize", windowWidth, windowHeight);
176         context.setViewPort(0, 0, windowWidth, windowHeight);
177         vertexArray.draw();
178         context.updateDisplay();
179     }
180 
181     public bool isCloseRequested() {
182         return context.isWindowCloseRequested();
183     }
184 
185     private Program makeProgram(string vertexShaderSource, string fragmentShaderSource) {
186         Shader vertexShader = context.newShader();
187         vertexShader.create();
188         vertexShader.setSource(new ShaderSource(vertexShaderSource, true));
189         vertexShader.compile();
190         Shader fragmentShader = context.newShader();
191         fragmentShader.create();
192         fragmentShader.setSource(new ShaderSource(fragmentShaderSource, true));
193         fragmentShader.compile();
194         Program program = context.newProgram();
195         program.create();
196         program.attachShader(vertexShader);
197         program.attachShader(fragmentShader);
198         program.link();
199         return program;
200     }
201 
202     private Texture makeTexture(Format format, InternalFormat internalFormat, int width, int height) {
203         Texture texture = context.newTexture();
204         texture.create();
205         texture.setFormat(format, internalFormat);
206         texture.setWraps(CLAMP_TO_BORDER, CLAMP_TO_BORDER);
207         texture.setBorderColor(0, 0, 0, 1);
208         texture.setFilters(NEAREST, NEAREST);
209         texture.setImageData(null, width, height);
210         return texture;
211     }
212 }
213 
214 public enum FilteringMode {
215     NONE,
216     LINEAR
217 }
218 
219 public enum UpscalingMode {
220     NONE,
221     EPX,
222     XBR,
223     BICUBIC
224 }
225 
226 private VertexData generatePlane(float width, float height) {
227     width /= 2;
228     height /= 2;
229     VertexData vertexData = new VertexData();
230     VertexAttribute positionsAttribute = new VertexAttribute("positions", FLOAT, 3);
231     vertexData.addAttribute(0, positionsAttribute);
232     float[] positions = [
233         -width, -height, 0,
234         width, -height, 0,
235         -width, height, 0,
236         width, height, 0
237     ];
238     positionsAttribute.setData(cast(ubyte[]) positions);
239     uint[] indices = [0, 3, 2, 0, 1, 3];
240     vertexData.setIndices(indices);
241     return vertexData;
242 }