koi moved to separate module
[dd2d.git] / render.d
blob64b8a1c6a8f860d4e98949b5f61b34bcfd0fc3e4
1 /* DooM2D: Midnight on the Firing Line
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module render is aliced;
20 //version = dont_use_vsync;
21 //version = old_light;
23 private:
24 import core.atomic;
25 import core.thread;
26 import core.time;
28 import std.concurrency;
30 import iv.glbinds;
31 import glutils;
32 import console;
33 import wadarc;
35 import iv.stream;
37 import d2dmap;
38 import d2dadefs;
39 //import d2dactors;
40 import d2dgfx;
41 import d2dfont;
42 import dacs;
44 import d2dunigrid;
46 // `map` is there
47 import dengapi;
49 import d2dparts;
52 // ////////////////////////////////////////////////////////////////////////// //
53 import arsd.color;
54 import arsd.png;
57 // ////////////////////////////////////////////////////////////////////////// //
58 public __gshared bool cheatNoDoors = false;
61 // ////////////////////////////////////////////////////////////////////////// //
62 public __gshared SimpleWindow sdwindow;
65 public enum vlWidth = 800;
66 public enum vlHeight = 800;
67 __gshared int scale = 2;
69 public int getScale () nothrow @trusted @nogc { pragma(inline, true); return scale; }
72 // ////////////////////////////////////////////////////////////////////////// //
73 __gshared bool levelLoaded = false;
76 // ////////////////////////////////////////////////////////////////////////// //
77 __gshared bool scanlines = false;
78 __gshared bool doLighting = true;
81 // ////////////////////////////////////////////////////////////////////////// //
82 // interpolation
83 __gshared ubyte[] prevFrameActorsData;
84 __gshared uint[65536] prevFrameActorOfs; // uint.max-1: dead; uint.max: last
85 __gshared MonoTime lastthink = MonoTime.zero; // for interpolator
86 __gshared MonoTime nextthink = MonoTime.zero;
87 __gshared bool frameInterpolation = true;
90 __gshared int[2] mapViewPosX, mapViewPosY; // [0]: previous frame -- for interpolator
93 // this should be screen center
94 public void setMapViewPos (int x, int y) {
95 if (map is null) {
96 mapViewPosX[] = 0;
97 mapViewPosY[] = 0;
98 return;
100 int swdt = vlWidth/scale;
101 int shgt = vlHeight/scale;
102 x -= swdt/2;
103 y -= shgt/2;
104 if (x < 0) x = 0; else if (x >= map.width*8-swdt) x = map.width*8-swdt-1;
105 if (y < 0) y = 0; else if (y >= map.height*8-shgt) y = map.height*8-shgt-1;
106 mapViewPosX[1] = x*scale;
107 mapViewPosY[1] = y*scale;
111 // ////////////////////////////////////////////////////////////////////////// //
112 // attached lights
113 struct AttachedLightInfo {
114 int x, y;
115 float r, g, b;
116 bool uncolored;
117 int radius;
120 __gshared AttachedLightInfo[65536] attachedLights;
121 __gshared uint attachedLightCount = 0;
124 // ////////////////////////////////////////////////////////////////////////// //
125 enum MaxLightRadius = 512;
128 // ////////////////////////////////////////////////////////////////////////// //
129 // for light
130 __gshared FBO[MaxLightRadius+1] fboOccluders, fboDistMap;
131 __gshared Shader shadToPolar, shadBlur, shadBlurOcc;
133 __gshared FBO fboLevel, fboLevelLight, fboOrigBack;
134 __gshared Shader shadScanlines;
135 __gshared Shader shadLiquidDistort;
138 // ////////////////////////////////////////////////////////////////////////// //
139 // call once!
140 public void initOpenGL () {
141 gloStackClear();
143 glEnable(GL_TEXTURE_2D);
144 glDisable(GL_LIGHTING);
145 glDisable(GL_DITHER);
146 glDisable(GL_BLEND);
147 glDisable(GL_DEPTH_TEST);
149 // create shaders
150 shadScanlines = new Shader("scanlines", loadTextFile("data/shaders/srscanlines.frag"));
152 shadLiquidDistort = new Shader("liquid_distort", loadTextFile("data/shaders/srliquid_distort.frag"));
153 shadLiquidDistort.exec((Shader shad) {
154 shad["texLqMap"] = 0;
157 // lights
158 version(old_light) {
159 shadToPolar = new Shader("light_topolar", loadTextFile("data/shaders/srlight_topolar.frag"));
160 } else {
161 shadToPolar = new Shader("light_topolar", loadTextFile("data/shaders/srlight_topolar_new.frag"));
163 shadToPolar.exec((Shader shad) {
164 shad["texOcc"] = 0;
165 shad["texOccFull"] = 2;
168 shadBlur = new Shader("light_blur", loadTextFile("data/shaders/srlight_blur.frag"));
169 shadBlur.exec((Shader shad) {
170 shad["texDist"] = 0;
171 shad["texBg"] = 1;
172 shad["texOcc"] = 2;
175 shadBlurOcc = new Shader("light_blur_occ", loadTextFile("data/shaders/srlight_blur_occ.frag"));
176 shadBlurOcc.exec((Shader shad) {
177 shad["texLMap"] = 0;
178 shad["texBg"] = 1;
179 shad["texOcc"] = 2;
182 //TODO: this sux!
183 foreach (int sz; 2..MaxLightRadius+1) {
184 fboOccluders[sz] = new FBO(sz*2, sz*2, Texture.Option.Clamp, Texture.Option.Linear); // create occluders FBO
185 fboDistMap[sz] = new FBO(sz*2, 1, Texture.Option.Clamp); // create 1d distance map FBO
188 // setup matrices
189 glMatrixMode(GL_MODELVIEW);
190 glLoadIdentity();
192 loadSmFont();
193 loadAllMonsterGraphics();
197 // ////////////////////////////////////////////////////////////////////////// //
198 // should be called when OpenGL is initialized
199 void loadMap (string mapname) {
200 if (map !is null) map.clear();
201 map = new LevelMap(mapname);
203 ugInit(map.width*8, map.height*8);
205 map.oglBuildMega();
206 mapTilesChanged = 0;
208 if (fboLevel !is null) fboLevel.clear();
209 if (fboLevelLight !is null) fboLevelLight.clear();
210 if (fboOrigBack !is null) fboOrigBack.clear();
212 fboLevel = new FBO(map.width*8, map.height*8, Texture.Option.Nearest); // final level render will be here
213 fboLevelLight = new FBO(map.width*8, map.height*8, Texture.Option.Nearest); // level lights will be rendered here
214 fboOrigBack = new FBO(map.width*8, map.height*8, Texture.Option.Nearest, Texture.Option.Depth); // background+foreground
216 shadToPolar.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8-1, map.height*8-1); });
217 shadBlur.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8, map.height*8); });
218 shadBlurOcc.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8, map.height*8); });
220 glActiveTexture(GL_TEXTURE0+0);
221 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
222 orthoCamera(vlWidth, vlHeight);
224 Actor.resetStorage();
225 loadMapMonsters();
227 dotInit();
229 // save first snapshot
230 prevFrameActorsData = new ubyte[](Actor.actorSize*65536); // ~15-20 megabytes
231 prevFrameActorOfs[] = uint.max; // just for fun
232 Actor.saveSnapshot(prevFrameActorsData[], prevFrameActorOfs.ptr);
233 mapViewPosX[0] = mapViewPosX[1];
234 mapViewPosY[0] = mapViewPosY[1];
236 levelLoaded = true;
238 { import core.memory : GC; GC.collect(); }
242 // ////////////////////////////////////////////////////////////////////////// //
243 //FIXME: optimize!
244 __gshared uint mapTilesChanged = 0;
247 //WARNING! this can be called only from DACS, so we don't have to sync it!
248 public void mapDirty (uint layermask) { mapTilesChanged |= layermask; }
251 void rebuildMapMegaTextures () {
252 //fbo.replaceTexture
253 //mapTilesChanged = false;
254 //map.clearMegaTextures();
255 map.oglBuildMega(mapTilesChanged);
256 mapTilesChanged = 0;
260 // ////////////////////////////////////////////////////////////////////////// //
261 // messages
262 struct Message {
263 enum Phase { FadeIn, Stay, FadeOut }
264 Phase phase;
265 int alpha;
266 int pauseMsecs;
267 MonoTime removeTime;
268 char[256] text;
269 usize textlen;
272 private import core.sync.mutex : Mutex;
274 __gshared Message[128] messages;
275 __gshared uint messagesUsed = 0;
277 //__gshared Mutex messageLock;
278 //shared static this () { messageLock = new Mutex(); }
281 void addMessage (const(char)[] msgtext, int pauseMsecs=3000, bool noreplace=false) {
282 //messageLock.lock();
283 //scope(exit) messageLock.unlock();
284 if (msgtext.length == 0) return;
285 conwriteln(msgtext);
286 if (pauseMsecs <= 50) return;
287 if (messagesUsed == messages.length) {
288 // remove top message
289 foreach (immutable cidx; 1..messagesUsed) messages.ptr[cidx-1] = messages.ptr[cidx];
290 messages.ptr[0].alpha = 255;
291 --messagesUsed;
293 // quick replace
294 if (!noreplace && messagesUsed == 1) {
295 switch (messages.ptr[0].phase) {
296 case Message.Phase.FadeIn:
297 messages.ptr[0].phase = Message.Phase.FadeOut;
298 break;
299 case Message.Phase.Stay:
300 messages.ptr[0].phase = Message.Phase.FadeOut;
301 messages.ptr[0].alpha = 255;
302 break;
303 default:
306 auto msg = messages.ptr+messagesUsed;
307 ++messagesUsed;
308 msg.phase = Message.Phase.FadeIn;
309 msg.alpha = 0;
310 msg.pauseMsecs = pauseMsecs;
311 // copy text
312 if (msgtext.length > msg.text.length) {
313 msg.text = msgtext[0..msg.text.length];
314 msg.textlen = msg.text.length;
315 } else {
316 msg.text[0..msgtext.length] = msgtext[];
317 msg.textlen = msgtext.length;
322 void doMessages (MonoTime curtime) {
323 //messageLock.lock();
324 //scope(exit) messageLock.unlock();
326 if (messagesUsed == 0) return;
327 glEnable(GL_BLEND);
328 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
330 Message* msg;
332 again:
333 msg = messages.ptr;
334 final switch (msg.phase) {
335 case Message.Phase.FadeIn:
336 if ((msg.alpha += 10) >= 255) {
337 msg.phase = Message.Phase.Stay;
338 msg.removeTime = curtime+dur!"msecs"(msg.pauseMsecs);
339 goto case; // to stay
341 glColor4f(1.0f, 1.0f, 1.0f, msg.alpha/255.0f);
342 break;
343 case Message.Phase.Stay:
344 if (msg.removeTime <= curtime) {
345 msg.alpha = 255;
346 msg.phase = Message.Phase.FadeOut;
347 goto case; // to fade
349 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
350 break;
351 case Message.Phase.FadeOut:
352 if ((msg.alpha -= 10) <= 0) {
353 if (--messagesUsed == 0) return;
354 // remove this message
355 foreach (immutable cidx; 1..messagesUsed+1) messages.ptr[cidx-1] = messages.ptr[cidx];
356 goto again;
358 glColor4f(1.0f, 1.0f, 1.0f, msg.alpha/255.0f);
359 break;
362 smDrawText(10, 10, msg.text[0..msg.textlen]);
366 // ////////////////////////////////////////////////////////////////////////// //
367 //mixin(Actor.FieldPropMixin!("0drawlistpos", uint));
369 mixin(Actor.FieldGetMixin!("classtype", StrId)); // fget_classtype
370 mixin(Actor.FieldGetMixin!("classname", StrId)); // fget_classname
371 mixin(Actor.FieldGetMixin!("x", int));
372 mixin(Actor.FieldGetMixin!("y", int));
373 mixin(Actor.FieldGetMixin!("flags", uint));
374 mixin(Actor.FieldGetMixin!("zAnimstate", StrId));
375 mixin(Actor.FieldGetMixin!("zAnimidx", int));
376 mixin(Actor.FieldGetMixin!("dir", uint));
377 mixin(Actor.FieldGetMixin!("attLightXOfs", int));
378 mixin(Actor.FieldGetMixin!("attLightYOfs", int));
379 mixin(Actor.FieldGetMixin!("attLightRGBX", uint));
381 //mixin(Actor.FieldGetPtrMixin!("classtype", StrId)); // fget_classtype
382 //mixin(Actor.FieldGetPtrMixin!("classname", StrId)); // fget_classname
383 mixin(Actor.FieldGetPtrMixin!("x", int));
384 mixin(Actor.FieldGetPtrMixin!("y", int));
385 //mixin(Actor.FieldGetPtrMixin!("flags", uint));
386 //mixin(Actor.FieldGetPtrMixin!("zAnimstate", StrId));
387 //mixin(Actor.FieldGetPtrMixin!("zAnimidx", int));
388 //mixin(Actor.FieldGetPtrMixin!("dir", uint));
389 //mixin(Actor.FieldGetPtrMixin!("attLightXOfs", int));
390 //mixin(Actor.FieldGetPtrMixin!("attLightYOfs", int));
391 //mixin(Actor.FieldGetPtrMixin!("attLightRGBX", uint));
394 // ////////////////////////////////////////////////////////////////////////// //
395 __gshared int vportX0, vportY0, vportX1, vportY1;
398 void renderLight() (int lightX, int lightY, in auto ref SVec4F lcol, int lightRadius) {
399 if (lightRadius < 2) return;
400 if (lightRadius > MaxLightRadius) lightRadius = MaxLightRadius;
401 int lightSize = lightRadius*2;
402 // is this light visible?
403 if (lightX <= -lightRadius || lightY <= -lightRadius || lightX-lightRadius >= map.width*8 || lightY-lightRadius >= map.height*8) return;
405 // out of viewport -- do nothing
406 if (lightX+lightRadius < vportX0 || lightY+lightRadius < vportY0) return;
407 if (lightX-lightRadius > vportX1 || lightY-lightRadius > vportY1) return;
409 if (lightX >= 0 && lightY >= 0 && lightX < map.width*8 && lightY < map.height*8 &&
410 map.teximgs[map.LightMask].imageData.colors.ptr[lightY*(map.width*8)+lightX].a > 190) return;
412 //glUseProgram(0);
413 glDisable(GL_BLEND);
415 // draw shadow casters to fboOccludersId, light should be in the center
416 version(old_light) {
417 fboOccluders[lightRadius].exec({
418 glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
419 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
420 glClear(GL_COLOR_BUFFER_BIT);
421 orthoCamera(lightSize, lightSize);
422 drawAtXY(map.texgl.ptr[map.LightMask], lightRadius-lightX, lightRadius-lightY);
425 // common color for all the following
426 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
428 // build 1d distance map to fboShadowMapId
429 fboDistMap[lightRadius].exec({
430 shadToPolar.exec((Shader shad) {
431 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
432 shad["lightPos"] = SVec2F(lightX, lightY);
433 // no need to clear it, shader will take care of that
434 orthoCamera(lightSize, 1);
435 version(old_light) {
436 drawAtXY(fboOccluders[lightRadius].tex.tid, 0, 0, lightSize, 1);
437 } else {
438 // it doesn't matter what we will draw here, so just draw filled rect
439 glRectf(0, 0, lightSize, 1);
444 // build light texture for blending
445 fboOccluders[lightRadius].exec({
446 // no need to clear it, shader will take care of that
447 shadBlur.exec((Shader shad) {
448 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
449 shad["lightColor"] = SVec4F(lcol.x, lcol.y, lcol.z, lcol.w);
450 shad["lightPos"] = SVec2F(lightX, lightY);
451 orthoCamera(lightSize, lightSize);
452 drawAtXY(fboDistMap[lightRadius].tex.tid, 0, 0, lightSize, lightSize);
456 // blend light texture
457 fboLevelLight.exec({
458 glEnable(GL_BLEND);
459 //glDisable(GL_BLEND);
460 glBlendFunc(GL_SRC_ALPHA, GL_ONE);
461 orthoCamera(map.width*8, map.height*8);
462 drawAtXY(fboOccluders[lightRadius].tex, lightX-lightRadius, lightY-lightRadius, mirrorY:true);
463 // and blend it again, with the shader that will touch only occluders
464 shadBlurOcc.exec((Shader shad) {
465 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
466 shad["lightColor"] = SVec4F(lcol.x, lcol.y, lcol.z, lcol.w);
467 shad["lightPos"] = SVec2F(lightX, lightY);
468 drawAtXY(fboOccluders[lightRadius].tex.tid, lightX-lightRadius, lightY-lightRadius, lightSize, lightSize, mirrorY:true);
474 // ////////////////////////////////////////////////////////////////////////// //
475 __gshared int testLightX = vlWidth/2, testLightY = vlHeight/2;
476 __gshared bool testLightMoved = true;
477 //__gshared int mapOfsX, mapOfsY;
478 //__gshared bool movement = false;
479 __gshared float iLiquidTime = 0.0;
480 //__gshared bool altMove = false;
483 void renderScene (MonoTime curtime) {
484 //enum BackIntens = 0.05f;
485 enum BackIntens = 0.0f;
487 gloStackClear();
488 float atob = (curtime > lastthink ? cast(float)((curtime-lastthink).total!"msecs")/cast(float)((nextthink-lastthink).total!"msecs") : 1.0f);
489 //{ import core.stdc.stdio; printf("atob=%f\n", cast(double)atob); }
492 int framelen = cast(int)((nextthink-lastthink).total!"msecs");
493 int curfp = cast(int)((curtime-lastthink).total!"msecs");
494 { import core.stdc.stdio; printf("framelen=%d; curfp=%d\n", framelen, curfp); }
498 int mofsx, mofsy;
500 bool camCenter = true;
501 if (/*altMove || movement ||*/ scale == 1) {
503 mofsx = mapOfsX;
504 mofsy = mapOfsY;
506 vportX0 = 0;
507 vportY0 = 0;
508 vportX1 = map.width*8;
509 vportY1 = map.height*8;
510 camCenter = false;
511 } else {
512 if (frameInterpolation) {
513 import core.stdc.math : roundf;
514 mofsx = cast(int)(mapViewPosX[0]+roundf((mapViewPosX[1]-mapViewPosX[0])*atob));
515 mofsy = cast(int)(mapViewPosY[0]+roundf((mapViewPosY[1]-mapViewPosY[0])*atob));
516 } else {
517 mofsx = mapViewPosX[1];
518 mofsy = mapViewPosY[1];
520 vportX0 = mofsx/scale;
521 vportY0 = mofsy/scale;
522 vportX1 = vportX0+vlWidth/scale;
523 vportY1 = vportY0+vlHeight/scale;
526 if (mapTilesChanged != 0) rebuildMapMegaTextures();
528 dotDraw(atob);
530 // build background layer
531 fboOrigBack.exec({
532 //glDisable(GL_BLEND);
533 //glClearDepth(1.0f);
534 //glDepthFunc(GL_LESS);
535 //glDepthFunc(GL_NEVER);
536 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
537 glClear(GL_COLOR_BUFFER_BIT/*|GL_DEPTH_BUFFER_BIT*/);
538 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
539 orthoCamera(map.width*8, map.height*8);
541 glEnable(GL_BLEND);
542 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
543 // draw sky
545 drawAtXY(map.skytexgl.tid, mapOfsX/2-map.MapSize*8, mapOfsY/2-map.MapSize*8, map.MapSize*8, map.MapSize*8);
546 drawAtXY(map.skytexgl.tid, mapOfsX/2-map.MapSize*8, mapOfsY/2, map.MapSize*8, map.MapSize*8);
547 drawAtXY(map.skytexgl.tid, mapOfsX/2, mapOfsY/2-map.MapSize*8, map.MapSize*8, map.MapSize*8);
548 drawAtXY(map.skytexgl.tid, mapOfsX/2, mapOfsY/2, map.MapSize*8, map.MapSize*8);
550 drawAtXY(map.skytexgl.tid, 0, 0, map.MapSize*8, map.MapSize*8);
551 // draw background
552 drawAtXY(map.texgl.ptr[map.Back], 0, 0);
553 // draw distorted liquid areas
554 shadLiquidDistort.exec((Shader shad) {
555 shad["iDistortTime"] = iLiquidTime;
556 drawAtXY(map.texgl.ptr[map.AllLiquids], 0, 0);
558 // monsters, items; we'll do linear interpolation here
559 glColor3f(1.0f, 1.0f, 1.0f);
560 //glEnable(GL_DEPTH_TEST);
561 attachedLightCount = 0;
563 // who cares about memory?!
564 // draw order: players, items, monsters, other
565 static struct DrawInfo {
566 ActorDef adef;
567 ActorId aid;
568 int actorX, actorY;
569 @disable this (this); // no copies
571 enum { Players, Items, Monsters, Other }
572 __gshared DrawInfo[65536][4] drawlists;
573 __gshared uint[4] dlpos;
574 DrawInfo camchickdi;
576 dlpos[] = 0;
577 ActorId camchick;
579 Actor.forEach((ActorId me) {
580 //me.fprop_0drawlistpos = 0;
581 if (auto adef = findActorDef(me)) {
582 uint dlnum = Other;
583 switch (adef.classtype.get) {
584 case "monster": dlnum = (adef.classname.get != "Player" ? Monsters : Players); break;
585 case "item": dlnum = Items; break;
586 default: dlnum = Other; break;
588 int actorX, actorY; // current actor position
590 auto ofs = prevFrameActorOfs.ptr[me.id&0xffff];
591 if (frameInterpolation && ofs < uint.max-1 && Actor.isSameSavedActor(me.id, prevFrameActorsData.ptr, ofs)) {
592 import core.stdc.math : roundf;
593 auto xptr = prevFrameActorsData.ptr+ofs;
594 int ox = xptr.fgetp_x;
595 int nx = me.fget_x;
596 int oy = xptr.fgetp_y;
597 int ny = me.fget_y;
598 actorX = cast(int)(ox+roundf((nx-ox)*atob));
599 actorY = cast(int)(oy+roundf((ny-oy)*atob));
600 //conwriteln("actor ", me.id, "; o=(", ox, ",", oy, "); n=(", nx, ",", ny, "); p=(", x, ",", y, ")");
601 } else {
602 actorX = me.fget_x;
603 actorY = me.fget_y;
606 if (!camchick.valid && me.flags!uint&AF_CAMERACHICK) {
607 camchick = me;
608 camchickdi.adef = adef;
609 camchickdi.aid = me;
610 camchickdi.actorX = actorX;
611 camchickdi.actorY = actorY;
613 // draw sprite
614 if ((me.fget_flags&AF_NODRAW) == 0) {
615 //auto dl = &drawlists[dlnum][dlpos.ptr[dlnum]];
616 //me.fprop_0drawlistpos = (dlpos.ptr[dlnum]&0xffff)|((dlnum&0xff)<<16);
617 auto dl = drawlists.ptr[dlnum].ptr+dlpos.ptr[dlnum];
618 ++dlpos.ptr[dlnum];
619 dl.adef = adef;
620 dl.aid = me;
621 dl.actorX = actorX;
622 dl.actorY = actorY;
624 if (auto isp = adef.animSpr(me.fget_zAnimstate, me.fget_dir, me.fget_zAnimidx)) {
625 drawAtXY(isp.tex, actorX-isp.vga.sx, actorY-isp.vga.sy, adef.zz);
626 } else {
627 //conwriteln("no animation for actor ", me.id, " (", me.classtype!string, ":", me.classname!string, ")");
631 // process attached lights
632 if ((me.fget_flags&AF_NOLIGHT) == 0) {
633 uint alr = me.fget_attLightRGBX;
634 if ((alr&0xff) >= 4) {
635 // yep, add it
636 auto li = attachedLights.ptr+attachedLightCount;
637 ++attachedLightCount;
638 li.x = actorX+me.fget_attLightXOfs;
639 li.y = actorY+me.fget_attLightYOfs;
640 li.r = ((alr>>24)&0xff)/255.0f; // red or intensity
641 if ((alr&0x00_ff_ff_00U) == 0x00_00_01_00U) {
642 li.uncolored = true;
643 } else {
644 li.g = ((alr>>16)&0xff)/255.0f;
645 li.b = ((alr>>8)&0xff)/255.0f;
646 li.uncolored = false;
648 li.radius = (alr&0xff);
649 if (li.radius > MaxLightRadius) li.radius = MaxLightRadius;
652 } else {
653 conwriteln("not found actor ", me.id, " (", me.classtype!string, ":", me.classname!string, ")");
656 // draw actor lists
657 foreach_reverse (uint dlnum; 0..drawlists.length) {
658 auto dl = drawlists.ptr[dlnum].ptr;
659 if (dlnum == Players) dl += dlpos.ptr[dlnum]-1;
660 foreach (uint idx; 0..dlpos.ptr[dlnum]) {
661 auto me = dl.aid;
662 if (auto isp = dl.adef.animSpr(me.fget_zAnimstate, me.fget_dir, me.fget_zAnimidx)) {
663 //drawAtXY(isp.tex, dl.actorX-isp.vga.sx, dl.actorY-isp.vga.sy);
664 isp.drawAtXY(dl.actorX, dl.actorY);
666 if (dlnum != Players) ++dl; else --dl;
669 if (camCenter && camchick.valid) {
670 int tiltHeight = /*getMapViewHeight()*/(vlHeight/scale)/4;
671 int vy = camchick.looky!int;
672 if (vy < -tiltHeight) vy = -tiltHeight; else if (vy > tiltHeight) vy = tiltHeight;
673 int swdt = vlWidth/scale;
674 int shgt = vlHeight/scale;
675 int x = camchickdi.actorX-swdt/2;
676 int y = (camchickdi.actorY+vy)-shgt/2;
677 if (x < 0) x = 0; else if (x >= map.width*8-swdt) x = map.width*8-swdt-1;
678 if (y < 0) y = 0; else if (y >= map.height*8-shgt) y = map.height*8-shgt-1;
679 mofsx = x*2;
680 mofsy = y*2;
681 vportX0 = mofsx/scale;
682 vportY0 = mofsy/scale;
683 vportX1 = vportX0+vlWidth/scale;
684 vportY1 = vportY0+vlHeight/scale;
686 //glDisable(GL_DEPTH_TEST);
687 // draw dots
688 drawAtXY(texParts, 0, 0);
689 // do liquid coloring
690 drawAtXY(map.texgl.ptr[map.LiquidMask], 0, 0);
691 // foreground -- hide secrets, draw lifts and such
692 drawAtXY(map.texgl.ptr[map.Front], 0, 0);
696 if (doLighting) {
697 // clear light layer
698 fboLevelLight.exec({
699 glDisable(GL_BLEND);
700 glClearColor(BackIntens, BackIntens, BackIntens, 1.0f);
701 //glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
702 ////glColor4f(1.0f, 1.0f, 1.0f, 0.0f);
703 glClear(GL_COLOR_BUFFER_BIT);
706 // texture 1 is background
707 glActiveTexture(GL_TEXTURE0+1);
708 glBindTexture(GL_TEXTURE_2D, fboOrigBack.tex.tid);
709 // texture 2 is occluders
710 glActiveTexture(GL_TEXTURE0+2);
711 glBindTexture(GL_TEXTURE_2D, map.texgl.ptr[map.LightMask].tid);
712 // done texture assign
713 glActiveTexture(GL_TEXTURE0+0);
715 enum LYOfs = 1;
717 renderLight( 27, 391-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 100);
718 renderLight(542, 424-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 100);
719 renderLight(377, 368-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 32);
720 renderLight(147, 288-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
721 renderLight( 71, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
722 renderLight(249, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
723 renderLight(426, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
724 renderLight(624, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
725 renderLight(549, 298-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
726 renderLight( 74, 304-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 32);
728 renderLight(24*8+4, (24+18)*8-2+LYOfs, SVec4F(0.6f, 0.0f, 0.0f, 1.0f), 128);
730 renderLight(280, 330, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
732 // attached lights
733 foreach (ref li; attachedLights[0..attachedLightCount]) {
734 if (li.uncolored) {
735 renderLight(li.x, li.y, SVec4F(0.0f, 0.0f, 0.0f, li.r), li.radius);
736 } else {
737 renderLight(li.x, li.y, SVec4F(li.r, li.g, li.b, 1.0f), li.radius);
742 if (testLightMoved) {
743 testLightX = testLightX/scale+mofsx/scale;
744 testLightY = testLightY/scale+mofsy/scale;
745 testLightMoved = false;
747 foreach (immutable _; 0..1) {
748 renderLight(testLightX, testLightY, SVec4F(0.3f, 0.3f, 0.0f, 1.0f), 96);
752 glActiveTexture(GL_TEXTURE0+1);
753 glBindTexture(GL_TEXTURE_2D, 0);
754 glActiveTexture(GL_TEXTURE0+2);
755 glBindTexture(GL_TEXTURE_2D, 0);
756 glActiveTexture(GL_TEXTURE0+0);
759 // draw scaled level
761 shadScanlines.exec((Shader shad) {
762 shad["scanlines"] = scanlines;
763 glClearColor(BackIntens, BackIntens, BackIntens, 1.0f);
764 glClear(GL_COLOR_BUFFER_BIT);
765 orthoCamera(vlWidth, vlHeight);
766 //orthoCamera(map.width*8*scale, map.height*8*scale);
767 //glMatrixMode(GL_MODELVIEW);
768 //glLoadIdentity();
769 //glTranslatef(0.375, 0.375, 0); // to be pixel-perfect
770 // somehow, FBO objects are mirrored; wtf?!
771 drawAtXY(fboLevel.tex.tid, -mapOfsX, -mapOfsY, map.width*8*scale, map.height*8*scale, mirrorY:true);
772 //glLoadIdentity();
777 fboLevelLight.exec({
778 smDrawText(map.width*8/2, map.height*8/2, "Testing...");
783 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
784 glDisable(GL_BLEND);
787 fboOrigBack.exec({
788 //auto img = smfont.ptr[0x39];
789 auto img = fftest;
790 if (img !is null) {
791 //conwriteln("img.sx=", img.sx, "; img.sy=", img.sy, "; ", img.width, "x", img.height);
792 drawAtXY(img.tex, 10-img.sx, 10-img.sy);
799 { // http://stackoverflow.com/questions/7207422/setting-up-opengl-multiple-render-targets
800 GLenum[1] buffers = [ GL_BACK_LEFT, /*GL_COLOR_ATTACHMENT0_EXT*/ ];
801 //GLenum[1] buffers = [ GL_NONE, /*GL_COLOR_ATTACHMENT0_EXT*/ ];
802 glDrawBuffers(1, buffers.ptr);
806 orthoCamera(vlWidth, vlHeight);
807 auto tex = (doLighting ? fboLevelLight.tex.tid : fboOrigBack.tex.tid);
808 //mofsx &= ~1;
809 //mofsy &= ~1;
810 drawAtXY(tex, -mofsx, -mofsy, map.width*8*scale, map.height*8*scale, mirrorY:true);
812 doMessages(curtime);
816 // ////////////////////////////////////////////////////////////////////////// //
817 // returns time slept
818 int sleepAtMaxMsecs (int msecs) {
819 if (msecs > 0) {
820 import core.sys.posix.signal : timespec;
821 import core.sys.posix.time : nanosleep;
822 timespec ts = void, tpassed = void;
823 ts.tv_sec = 0;
824 ts.tv_nsec = msecs*1000*1000+(500*1000); // milli to nano
825 nanosleep(&ts, &tpassed);
826 return (ts.tv_nsec-tpassed.tv_nsec)/(1000*1000);
827 } else {
828 return 0;
833 // ////////////////////////////////////////////////////////////////////////// //
834 // rendering thread
835 shared int diedie = 0;
837 enum D2DFrameTime = 55; // milliseconds
838 enum MinFrameTime = 1000/60; // ~60 FPS
840 public void renderThread (Tid starterTid) {
841 send(starterTid, 42);
842 try {
843 MonoTime curtime = MonoTime.currTime;
845 lastthink = curtime; // for interpolator
846 nextthink = curtime+dur!"msecs"(D2DFrameTime);
847 MonoTime nextvframe = curtime;
849 enum MaxFPSFrames = 16;
850 float frtimes = 0.0f;
851 int framenum = 0;
852 int prevFPS = -1;
853 int hushFrames = 6; // ignore first `hushFrames` frames overtime
854 MonoTime prevFrameStartTime = curtime;
856 bool vframeWasLost = false;
858 void receiveMessages () {
859 for (;;) {
860 import core.time : Duration;
861 //conwriteln("rendering thread: waiting for messages...");
862 auto got = receiveTimeout(
863 Duration.zero, // don't wait
864 (TMsgMessage msg) {
865 addMessage(msg.text[0..msg.textlen], msg.pauseMsecs, msg.noreplace);
867 (TMsgLoadLevel msg) {
868 if (levelLoaded) {
869 conwriteln("ERROR: can't load new levels yet");
870 return;
872 auto mn = msg.mapfile[0..msg.textlen].idup;
873 if (mn.length == 0) {
874 conwriteln("ERROR: can't load empty level!");
876 conwriteln("loading map '", mn, "'");
877 loadMap(mn);
879 (TMsgIntOption msg) {
880 final switch (msg.type) with (TMsgIntOption.Type) {
881 case Scale:
882 if (msg.value >= 1 && msg.value <= 2) scale = msg.value;
883 break;
886 (TMsgBoolOption msg) {
887 final switch (msg.type) with (TMsgBoolOption.Type) {
888 case Interpolation:
889 if (msg.toggle) msg.value = !frameInterpolation;
890 if (frameInterpolation != msg.value) {
891 frameInterpolation = msg.value;
892 } else {
893 msg.showMessage = false;
895 break;
896 case Lighting:
897 if (msg.toggle) msg.value = !doLighting;
898 if (doLighting != msg.value) {
899 doLighting = msg.value;
900 } else {
901 msg.showMessage = false;
903 break;
904 case CheatNoDoors:
905 if (msg.toggle) msg.value = !cheatNoDoors;
906 if (cheatNoDoors != msg.value) {
907 cheatNoDoors = msg.value;
908 } else {
909 msg.showMessage = false;
911 break;
913 if (msg.showMessage) {
914 char[128] mbuf;
915 uint msgpos;
916 void putStr(T) (T s) if (is(T : const(char)[])) {
917 foreach (char ch; s) {
918 if (msgpos >= mbuf.length) break;
919 mbuf.ptr[msgpos++] = ch;
922 putStr("Option \"");
923 { import std.conv : to; putStr(to!string(msg.type)); }
924 putStr("\": O");
925 if (msg.value) putStr("N"); else putStr("FF");
926 addMessage(mbuf[0..msgpos]);
929 (TMsgTestLightMove msg) {
930 testLightX = msg.x;
931 testLightY = msg.y;
932 testLightMoved = true;
934 (Variant v) {
935 conwriteln("WARNING: unknown thread message received and ignored");
938 if (!got) {
939 // no more messages
940 //conwriteln("rendering thread: no more messages");
941 break;
946 // "D2D frames"; curtime should be set; return `true` if frame was processed; will fix `curtime`
947 bool doThinkFrame () {
948 if (curtime >= nextthink) {
949 lastthink = curtime;
950 while (nextthink <= curtime) nextthink += dur!"msecs"(D2DFrameTime);
951 if (levelLoaded) {
952 // save snapshot and other data for interpolator
953 Actor.saveSnapshot(prevFrameActorsData[], prevFrameActorOfs.ptr);
954 mapViewPosX[0] = mapViewPosX[1];
955 mapViewPosY[0] = mapViewPosY[1];
956 // process actors
957 doActorsThink();
958 dotThink();
960 // some timing
961 auto tm = MonoTime.currTime;
962 int thinkTime = cast(int)((tm-curtime).total!"msecs");
963 if (thinkTime > 9) { import core.stdc.stdio; printf("spent on thinking: %d msecs\n", thinkTime); }
964 curtime = tm;
965 return true;
966 } else {
967 return false;
971 // "video frames"; curtime should be set; return `true` if frame was processed; will fix `curtime`
972 bool doVFrame () {
973 version(dont_use_vsync) {
974 // timer
975 enum doCheckTime = true;
976 } else {
977 // vsync
978 __gshared bool prevLost = false;
979 bool doCheckTime = vframeWasLost;
980 if (vframeWasLost) {
981 if (!prevLost) {
982 { import core.stdc.stdio; printf("frame was lost!\n"); }
984 prevLost = true;
985 } else {
986 prevLost = false;
989 if (doCheckTime) {
990 if (curtime < nextvframe) return false;
991 version(dont_use_vsync) {
992 if (curtime > nextvframe) {
993 auto overtime = cast(int)((curtime-nextvframe).total!"msecs");
994 if (overtime > 2500) {
995 if (hushFrames) {
996 --hushFrames;
997 } else {
998 { import core.stdc.stdio; printf(" spent whole %d msecs\n", overtime); }
1004 while (nextvframe <= curtime) nextvframe += dur!"msecs"(MinFrameTime);
1005 bool ctset = false;
1007 sdwindow.mtLock();
1008 scope(exit) sdwindow.mtUnlock();
1009 ctset = sdwindow.setAsCurrentOpenGlContextNT;
1011 // if we can't set context, pretend that videoframe was processed; this should solve problem with vsync and invisible window
1012 if (ctset) {
1013 // render scene
1014 iLiquidTime = cast(float)((curtime-MonoTime.zero).total!"msecs"%10000000)/18.0f*0.04f;
1015 receiveMessages(); // here, 'cause we need active OpenGL context for some messages
1016 if (levelLoaded) {
1017 renderScene(curtime);
1018 } else {
1019 //renderLoading(curtime);
1021 sdwindow.mtLock();
1022 scope(exit) sdwindow.mtUnlock();
1023 sdwindow.swapOpenGlBuffers();
1024 glFinish();
1025 sdwindow.releaseCurrentOpenGlContext();
1026 vframeWasLost = false;
1027 } else {
1028 vframeWasLost = true;
1029 { import core.stdc.stdio; printf("xframe was lost!\n"); }
1031 curtime = MonoTime.currTime;
1032 return true;
1035 for (;;) {
1036 if (sdwindow.closed) break;
1037 if (atomicLoad(diedie) > 0) break;
1039 curtime = MonoTime.currTime;
1040 auto fstime = curtime;
1042 doThinkFrame(); // this will fix curtime if necessary
1043 if (doVFrame()) {
1044 if (!vframeWasLost) {
1045 // fps
1046 auto frameTime = cast(float)(curtime-prevFrameStartTime).total!"msecs"/1000.0f;
1047 prevFrameStartTime = curtime;
1048 frtimes += frameTime;
1049 if (++framenum >= MaxFPSFrames || frtimes >= 3.0f) {
1050 import std.string : format;
1051 int newFPS = cast(int)(cast(float)MaxFPSFrames/frtimes+0.5);
1052 if (newFPS != prevFPS) {
1053 sdwindow.title = "%s / FPS:%s".format("D2D", newFPS);
1054 prevFPS = newFPS;
1056 framenum = 0;
1057 frtimes = 0.0f;
1062 curtime = MonoTime.currTime;
1064 // now sleep until next "video" or "think" frame
1065 if (nextthink > curtime && nextvframe > curtime) {
1066 // let's decide
1067 immutable nextVideoFrameSleep = cast(int)((nextvframe-curtime).total!"msecs");
1068 immutable nextThinkFrameSleep = cast(int)((nextthink-curtime).total!"msecs");
1069 immutable sleepTime = (nextVideoFrameSleep < nextThinkFrameSleep ? nextVideoFrameSleep : nextThinkFrameSleep);
1070 sleepAtMaxMsecs(sleepTime);
1071 //curtime = MonoTime.currTime;
1074 } catch (Exception e) {
1075 import core.stdc.stdio;
1076 fprintf(stderr, "FUUUUUUUUUUUUUUUUUUUUUUUUUU\n");
1077 for (;;) {
1078 if (sdwindow.closed) break;
1079 if (atomicLoad(diedie) > 0) break;
1080 sleepAtMaxMsecs(100);
1083 atomicStore(diedie, 2);
1087 // ////////////////////////////////////////////////////////////////////////// //
1088 __gshared Tid renderTid;
1089 shared bool renderThreadStarted = false;
1092 public void startRenderThread () {
1093 if (!cas(&renderThreadStarted, false, true)) {
1094 assert(0, "render thread already started!");
1096 renderTid = spawn(&renderThread, thisTid);
1097 setMaxMailboxSize(renderTid, 1024, OnCrowding.throwException); //FIXME
1098 // wait for "i'm ready" signal
1099 receive(
1100 (int ok) {
1101 if (ok != 42) assert(0, "wtf?!");
1104 conwriteln("rendering thread started");
1108 // ////////////////////////////////////////////////////////////////////////// //
1109 public void closeWindow () {
1110 if (atomicLoad(diedie) != 2) {
1111 atomicStore(diedie, 1);
1112 while (atomicLoad(diedie) != 2) {}
1114 if (!sdwindow.closed) {
1115 flushGui();
1116 sdwindow.close();
1121 // ////////////////////////////////////////////////////////////////////////// //
1122 // thread messages
1125 // ////////////////////////////////////////////////////////////////////////// //
1126 struct TMsgTestLightMove {
1127 int x, y;
1130 public void postTestLightMove (int x, int y) {
1131 if (!atomicLoad(renderThreadStarted)) return;
1132 auto msg = TMsgTestLightMove(x, y);
1133 send(renderTid, msg);
1137 // ////////////////////////////////////////////////////////////////////////// //
1138 struct TMsgMessage {
1139 char[256] text;
1140 uint textlen;
1141 int pauseMsecs;
1142 bool noreplace;
1145 public void postAddMessage (const(char)[] msgtext, int pauseMsecs=3000, bool noreplace=false) {
1146 if (!atomicLoad(renderThreadStarted)) return;
1147 if (msgtext.length > TMsgMessage.text.length) msgtext = msgtext[0..TMsgMessage.text.length];
1148 TMsgMessage msg;
1149 msg.textlen = cast(uint)msgtext.length;
1150 if (msg.textlen) msg.text[0..msg.textlen] = msgtext[0..msg.textlen];
1151 msg.pauseMsecs = pauseMsecs;
1152 msg.noreplace = noreplace;
1153 send(renderTid, msg);
1157 // ////////////////////////////////////////////////////////////////////////// //
1158 struct TMsgLoadLevel {
1159 char[1024] mapfile;
1160 uint textlen;
1163 public void postLoadLevel (const(char)[] mapfile) {
1164 if (!atomicLoad(renderThreadStarted)) return;
1165 if (mapfile.length > TMsgLoadLevel.mapfile.length) mapfile = mapfile[0..TMsgLoadLevel.mapfile.length];
1166 TMsgLoadLevel msg;
1167 msg.textlen = cast(uint)mapfile.length;
1168 if (msg.textlen) msg.mapfile[0..msg.textlen] = mapfile[0..msg.textlen];
1169 send(renderTid, msg);
1173 // ////////////////////////////////////////////////////////////////////////// //
1174 struct TMsgIntOption {
1175 enum Type {
1176 Scale,
1178 Type type;
1179 int value;
1180 bool showMessage;
1184 struct TMsgBoolOption {
1185 enum Type {
1186 Interpolation,
1187 Lighting,
1188 CheatNoDoors,
1190 Type type;
1191 bool value;
1192 bool toggle;
1193 bool showMessage;
1197 bool strCaseEqu (const(char)[] s0, const(char)[] s1) {
1198 if (s0.length != s1.length) return false;
1199 foreach (immutable idx, char ch; s0) {
1200 if (ch >= 'A' && ch <= 'Z') ch += 32;
1201 char c1 = s1.ptr[idx];
1202 if (c1 >= 'A' && c1 <= 'Z') c1 += 32;
1203 if (ch != c1) return false;
1205 return true;
1209 public bool postSetOption(T, ST) (ST name, T value, bool showMessage=false, bool toggle=false) if (is(ST : const(char)[]) && (is(T == int) || is(T == bool))) {
1210 if (!atomicLoad(renderThreadStarted)) return false;
1211 if (name.length == 0 || name.length > 127) return false;
1212 static if (is(T == int)) {
1213 TMsgIntOption msg;
1214 foreach (string oname; __traits(allMembers, TMsgIntOption.Type)) {
1215 if (strCaseEqu(oname, name)) {
1216 msg.type = __traits(getMember, TMsgIntOption.Type, oname);
1217 msg.value = value;
1218 msg.showMessage = showMessage;
1219 send(renderTid, msg);
1220 return true;
1223 } else static if (is(T == bool)) {
1224 TMsgBoolOption msg;
1225 foreach (string oname; __traits(allMembers, TMsgBoolOption.Type)) {
1226 if (strCaseEqu(oname, name)) {
1227 msg.type = __traits(getMember, TMsgBoolOption.Type, oname);
1228 msg.value = value;
1229 msg.toggle = toggle;
1230 msg.showMessage = showMessage;
1231 send(renderTid, msg);
1232 return true;
1235 } else {
1236 static assert(0, "invalid option type '"~T.stringof~"'");
1238 return false;