we can go to level 2 now!
[dd2d.git] / render.d
blobd9a5c3e6afddab57334ba52cead5df42ec06270c
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;
59 public __gshared bool cheatNoWallClip = false;
62 // ////////////////////////////////////////////////////////////////////////// //
63 public __gshared SimpleWindow sdwindow;
66 public enum vlWidth = 800;
67 public enum vlHeight = 800;
68 __gshared int scale = 2;
70 public int getScale () nothrow @trusted @nogc { pragma(inline, true); return scale; }
73 // ////////////////////////////////////////////////////////////////////////// //
74 __gshared bool levelLoaded = false;
77 // ////////////////////////////////////////////////////////////////////////// //
78 __gshared bool scanlines = false;
79 __gshared bool doLighting = true;
82 // ////////////////////////////////////////////////////////////////////////// //
83 // interpolation
84 __gshared ubyte[] prevFrameActorsData;
85 __gshared uint[65536] prevFrameActorOfs; // uint.max-1: dead; uint.max: last
86 __gshared MonoTime lastthink = MonoTime.zero; // for interpolator
87 __gshared MonoTime nextthink = MonoTime.zero;
88 __gshared bool frameInterpolation = true;
91 __gshared int[2] mapViewPosX, mapViewPosY; // [0]: previous frame -- for interpolator
94 // this should be screen center
95 public void setMapViewPos (int x, int y) {
96 if (map is null) {
97 mapViewPosX[] = 0;
98 mapViewPosY[] = 0;
99 return;
101 int swdt = vlWidth/scale;
102 int shgt = vlHeight/scale;
103 x -= swdt/2;
104 y -= shgt/2;
105 if (x < 0) x = 0; else if (x >= map.width*8-swdt) x = map.width*8-swdt-1;
106 if (y < 0) y = 0; else if (y >= map.height*8-shgt) y = map.height*8-shgt-1;
107 mapViewPosX[1] = x*scale;
108 mapViewPosY[1] = y*scale;
112 // ////////////////////////////////////////////////////////////////////////// //
113 // attached lights
114 struct AttachedLightInfo {
115 int x, y;
116 float r, g, b;
117 bool uncolored;
118 int radius;
121 __gshared AttachedLightInfo[65536] attachedLights;
122 __gshared uint attachedLightCount = 0;
125 // ////////////////////////////////////////////////////////////////////////// //
126 enum MaxLightRadius = 512;
129 // ////////////////////////////////////////////////////////////////////////// //
130 // for light
131 __gshared FBO[MaxLightRadius+1] fboOccluders, fboDistMap;
132 __gshared Shader shadToPolar, shadBlur, shadBlurOcc;
134 __gshared FBO fboLevel, fboLevelLight, fboOrigBack;
135 __gshared Shader shadScanlines;
136 __gshared Shader shadLiquidDistort;
139 // ////////////////////////////////////////////////////////////////////////// //
140 // call once!
141 public void initOpenGL () {
142 gloStackClear();
144 glEnable(GL_TEXTURE_2D);
145 glDisable(GL_LIGHTING);
146 glDisable(GL_DITHER);
147 glDisable(GL_BLEND);
148 glDisable(GL_DEPTH_TEST);
150 // create shaders
151 shadScanlines = new Shader("scanlines", loadTextFile("data/shaders/srscanlines.frag"));
153 shadLiquidDistort = new Shader("liquid_distort", loadTextFile("data/shaders/srliquid_distort.frag"));
154 shadLiquidDistort.exec((Shader shad) {
155 shad["texLqMap"] = 0;
158 // lights
159 version(old_light) {
160 shadToPolar = new Shader("light_topolar", loadTextFile("data/shaders/srlight_topolar.frag"));
161 } else {
162 shadToPolar = new Shader("light_topolar", loadTextFile("data/shaders/srlight_topolar_new.frag"));
164 shadToPolar.exec((Shader shad) {
165 shad["texOcc"] = 0;
166 shad["texOccFull"] = 2;
169 shadBlur = new Shader("light_blur", loadTextFile("data/shaders/srlight_blur.frag"));
170 shadBlur.exec((Shader shad) {
171 shad["texDist"] = 0;
172 shad["texBg"] = 1;
173 shad["texOcc"] = 2;
176 shadBlurOcc = new Shader("light_blur_occ", loadTextFile("data/shaders/srlight_blur_occ.frag"));
177 shadBlurOcc.exec((Shader shad) {
178 shad["texLMap"] = 0;
179 shad["texBg"] = 1;
180 shad["texOcc"] = 2;
183 //TODO: this sux!
184 foreach (int sz; 2..MaxLightRadius+1) {
185 fboOccluders[sz] = new FBO(sz*2, sz*2, Texture.Option.Clamp, Texture.Option.Linear); // create occluders FBO
186 fboDistMap[sz] = new FBO(sz*2, 1, Texture.Option.Clamp); // create 1d distance map FBO
189 // setup matrices
190 glMatrixMode(GL_MODELVIEW);
191 glLoadIdentity();
193 loadSmFont();
194 loadAllMonsterGraphics();
198 // ////////////////////////////////////////////////////////////////////////// //
199 // should be called when OpenGL is initialized
200 void loadMap (string mapname) {
201 if (map !is null) map.clear();
202 map = new LevelMap(mapname);
203 curmapname = mapname;
205 ugInit(map.width*8, map.height*8);
207 map.oglBuildMega();
208 mapTilesChanged = 0;
210 if (fboLevel !is null) fboLevel.clear();
211 if (fboLevelLight !is null) fboLevelLight.clear();
212 if (fboOrigBack !is null) fboOrigBack.clear();
214 fboLevel = new FBO(map.width*8, map.height*8, Texture.Option.Nearest); // final level render will be here
215 fboLevelLight = new FBO(map.width*8, map.height*8, Texture.Option.Nearest); // level lights will be rendered here
216 fboOrigBack = new FBO(map.width*8, map.height*8, Texture.Option.Nearest, Texture.Option.Depth); // background+foreground
218 shadToPolar.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8-1, map.height*8-1); });
219 shadBlur.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8, map.height*8); });
220 shadBlurOcc.exec((Shader shad) { shad["mapPixSize"] = SVec2F(map.width*8, map.height*8); });
222 glActiveTexture(GL_TEXTURE0+0);
223 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
224 orthoCamera(vlWidth, vlHeight);
226 Actor.resetStorage();
227 loadMapMonsters();
229 dotInit();
231 // save first snapshot
232 if (prevFrameActorsData.length == 0) prevFrameActorsData = new ubyte[](Actor.actorSize*65536); // ~15-20 megabytes
233 prevFrameActorOfs[] = uint.max; // just for fun
234 Actor.saveSnapshot(prevFrameActorsData[], prevFrameActorOfs.ptr);
235 mapViewPosX[0] = mapViewPosX[1];
236 mapViewPosY[0] = mapViewPosY[1];
238 levelLoaded = true;
240 { import core.memory : GC; GC.collect(); }
244 // ////////////////////////////////////////////////////////////////////////// //
245 //FIXME: optimize!
246 __gshared uint mapTilesChanged = 0;
249 //WARNING! this can be called only from DACS, so we don't have to sync it!
250 public void mapDirty (uint layermask) { mapTilesChanged |= layermask; }
253 void rebuildMapMegaTextures () {
254 //fbo.replaceTexture
255 //mapTilesChanged = false;
256 //map.clearMegaTextures();
257 map.oglBuildMega(mapTilesChanged);
258 mapTilesChanged = 0;
262 // ////////////////////////////////////////////////////////////////////////// //
263 // messages
264 struct Message {
265 enum Phase { FadeIn, Stay, FadeOut }
266 Phase phase;
267 int alpha;
268 int pauseMsecs;
269 MonoTime removeTime;
270 char[256] text;
271 usize textlen;
274 private import core.sync.mutex : Mutex;
276 __gshared Message[128] messages;
277 __gshared uint messagesUsed = 0;
279 //__gshared Mutex messageLock;
280 //shared static this () { messageLock = new Mutex(); }
283 void addMessage (const(char)[] msgtext, int pauseMsecs=3000, bool noreplace=false) {
284 //messageLock.lock();
285 //scope(exit) messageLock.unlock();
286 if (msgtext.length == 0) return;
287 conwriteln(msgtext);
288 if (pauseMsecs <= 50) return;
289 if (messagesUsed == messages.length) {
290 // remove top message
291 foreach (immutable cidx; 1..messagesUsed) messages.ptr[cidx-1] = messages.ptr[cidx];
292 messages.ptr[0].alpha = 255;
293 --messagesUsed;
295 // quick replace
296 if (!noreplace && messagesUsed == 1) {
297 switch (messages.ptr[0].phase) {
298 case Message.Phase.FadeIn:
299 messages.ptr[0].phase = Message.Phase.FadeOut;
300 break;
301 case Message.Phase.Stay:
302 messages.ptr[0].phase = Message.Phase.FadeOut;
303 messages.ptr[0].alpha = 255;
304 break;
305 default:
308 auto msg = messages.ptr+messagesUsed;
309 ++messagesUsed;
310 msg.phase = Message.Phase.FadeIn;
311 msg.alpha = 0;
312 msg.pauseMsecs = pauseMsecs;
313 // copy text
314 if (msgtext.length > msg.text.length) {
315 msg.text = msgtext[0..msg.text.length];
316 msg.textlen = msg.text.length;
317 } else {
318 msg.text[0..msgtext.length] = msgtext[];
319 msg.textlen = msgtext.length;
324 void doMessages (MonoTime curtime) {
325 //messageLock.lock();
326 //scope(exit) messageLock.unlock();
328 if (messagesUsed == 0) return;
329 glEnable(GL_BLEND);
330 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
332 Message* msg;
334 again:
335 msg = messages.ptr;
336 final switch (msg.phase) {
337 case Message.Phase.FadeIn:
338 if ((msg.alpha += 10) >= 255) {
339 msg.phase = Message.Phase.Stay;
340 msg.removeTime = curtime+dur!"msecs"(msg.pauseMsecs);
341 goto case; // to stay
343 glColor4f(1.0f, 1.0f, 1.0f, msg.alpha/255.0f);
344 break;
345 case Message.Phase.Stay:
346 if (msg.removeTime <= curtime) {
347 msg.alpha = 255;
348 msg.phase = Message.Phase.FadeOut;
349 goto case; // to fade
351 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
352 break;
353 case Message.Phase.FadeOut:
354 if ((msg.alpha -= 10) <= 0) {
355 if (--messagesUsed == 0) return;
356 // remove this message
357 foreach (immutable cidx; 1..messagesUsed+1) messages.ptr[cidx-1] = messages.ptr[cidx];
358 goto again;
360 glColor4f(1.0f, 1.0f, 1.0f, msg.alpha/255.0f);
361 break;
364 smDrawText(10, 10, msg.text[0..msg.textlen]);
368 // ////////////////////////////////////////////////////////////////////////// //
369 //mixin(Actor.FieldPropMixin!("0drawlistpos", uint));
371 mixin(Actor.FieldGetMixin!("classtype", StrId)); // fget_classtype
372 mixin(Actor.FieldGetMixin!("classname", StrId)); // fget_classname
373 mixin(Actor.FieldGetMixin!("x", int));
374 mixin(Actor.FieldGetMixin!("y", int));
375 mixin(Actor.FieldGetMixin!("flags", uint));
376 mixin(Actor.FieldGetMixin!("zAnimstate", StrId));
377 mixin(Actor.FieldGetMixin!("zAnimidx", int));
378 mixin(Actor.FieldGetMixin!("dir", uint));
379 mixin(Actor.FieldGetMixin!("attLightXOfs", int));
380 mixin(Actor.FieldGetMixin!("attLightYOfs", int));
381 mixin(Actor.FieldGetMixin!("attLightRGBX", uint));
383 //mixin(Actor.FieldGetPtrMixin!("classtype", StrId)); // fget_classtype
384 //mixin(Actor.FieldGetPtrMixin!("classname", StrId)); // fget_classname
385 mixin(Actor.FieldGetPtrMixin!("x", int));
386 mixin(Actor.FieldGetPtrMixin!("y", int));
387 //mixin(Actor.FieldGetPtrMixin!("flags", uint));
388 //mixin(Actor.FieldGetPtrMixin!("zAnimstate", StrId));
389 //mixin(Actor.FieldGetPtrMixin!("zAnimidx", int));
390 //mixin(Actor.FieldGetPtrMixin!("dir", uint));
391 //mixin(Actor.FieldGetPtrMixin!("attLightXOfs", int));
392 //mixin(Actor.FieldGetPtrMixin!("attLightYOfs", int));
393 //mixin(Actor.FieldGetPtrMixin!("attLightRGBX", uint));
396 // ////////////////////////////////////////////////////////////////////////// //
397 __gshared int vportX0, vportY0, vportX1, vportY1;
400 void renderLight() (int lightX, int lightY, in auto ref SVec4F lcol, int lightRadius) {
401 if (lightRadius < 2) return;
402 if (lightRadius > MaxLightRadius) lightRadius = MaxLightRadius;
403 int lightSize = lightRadius*2;
404 // is this light visible?
405 if (lightX <= -lightRadius || lightY <= -lightRadius || lightX-lightRadius >= map.width*8 || lightY-lightRadius >= map.height*8) return;
407 // out of viewport -- do nothing
408 if (lightX+lightRadius < vportX0 || lightY+lightRadius < vportY0) return;
409 if (lightX-lightRadius > vportX1 || lightY-lightRadius > vportY1) return;
411 if (lightX >= 0 && lightY >= 0 && lightX < map.width*8 && lightY < map.height*8 &&
412 map.teximgs[map.LightMask].imageData.colors.ptr[lightY*(map.width*8)+lightX].a > 190) return;
414 //glUseProgram(0);
415 glDisable(GL_BLEND);
417 // draw shadow casters to fboOccludersId, light should be in the center
418 version(old_light) {
419 fboOccluders[lightRadius].exec({
420 glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
421 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
422 glClear(GL_COLOR_BUFFER_BIT);
423 orthoCamera(lightSize, lightSize);
424 drawAtXY(map.texgl.ptr[map.LightMask], lightRadius-lightX, lightRadius-lightY);
427 // common color for all the following
428 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
430 // build 1d distance map to fboShadowMapId
431 fboDistMap[lightRadius].exec({
432 shadToPolar.exec((Shader shad) {
433 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
434 shad["lightPos"] = SVec2F(lightX, lightY);
435 // no need to clear it, shader will take care of that
436 orthoCamera(lightSize, 1);
437 version(old_light) {
438 drawAtXY(fboOccluders[lightRadius].tex.tid, 0, 0, lightSize, 1);
439 } else {
440 // it doesn't matter what we will draw here, so just draw filled rect
441 glRectf(0, 0, lightSize, 1);
446 // build light texture for blending
447 fboOccluders[lightRadius].exec({
448 // no need to clear it, shader will take care of that
449 shadBlur.exec((Shader shad) {
450 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
451 shad["lightColor"] = SVec4F(lcol.x, lcol.y, lcol.z, lcol.w);
452 shad["lightPos"] = SVec2F(lightX, lightY);
453 orthoCamera(lightSize, lightSize);
454 drawAtXY(fboDistMap[lightRadius].tex.tid, 0, 0, lightSize, lightSize);
458 // blend light texture
459 fboLevelLight.exec({
460 glEnable(GL_BLEND);
461 //glDisable(GL_BLEND);
462 glBlendFunc(GL_SRC_ALPHA, GL_ONE);
463 orthoCamera(map.width*8, map.height*8);
464 drawAtXY(fboOccluders[lightRadius].tex, lightX-lightRadius, lightY-lightRadius, mirrorY:true);
465 // and blend it again, with the shader that will touch only occluders
466 shadBlurOcc.exec((Shader shad) {
467 shad["lightTexSize"] = SVec2F(lightSize, lightSize);
468 shad["lightColor"] = SVec4F(lcol.x, lcol.y, lcol.z, lcol.w);
469 shad["lightPos"] = SVec2F(lightX, lightY);
470 drawAtXY(fboOccluders[lightRadius].tex.tid, lightX-lightRadius, lightY-lightRadius, lightSize, lightSize, mirrorY:true);
476 // ////////////////////////////////////////////////////////////////////////// //
477 __gshared int testLightX = vlWidth/2, testLightY = vlHeight/2;
478 __gshared bool testLightMoved = false;
479 //__gshared int mapOfsX, mapOfsY;
480 //__gshared bool movement = false;
481 __gshared float iLiquidTime = 0.0;
482 //__gshared bool altMove = false;
485 void renderScene (MonoTime curtime) {
486 //enum BackIntens = 0.05f;
487 enum BackIntens = 0.0f;
489 gloStackClear();
490 float atob = (curtime > lastthink ? cast(float)((curtime-lastthink).total!"msecs")/cast(float)((nextthink-lastthink).total!"msecs") : 1.0f);
491 //{ import core.stdc.stdio; printf("atob=%f\n", cast(double)atob); }
494 int framelen = cast(int)((nextthink-lastthink).total!"msecs");
495 int curfp = cast(int)((curtime-lastthink).total!"msecs");
496 { import core.stdc.stdio; printf("framelen=%d; curfp=%d\n", framelen, curfp); }
500 int mofsx, mofsy;
502 bool camCenter = true;
503 if (/*altMove || movement ||*/ scale == 1) {
505 mofsx = mapOfsX;
506 mofsy = mapOfsY;
508 vportX0 = 0;
509 vportY0 = 0;
510 vportX1 = map.width*8;
511 vportY1 = map.height*8;
512 camCenter = false;
513 } else {
514 if (frameInterpolation) {
515 import core.stdc.math : roundf;
516 mofsx = cast(int)(mapViewPosX[0]+roundf((mapViewPosX[1]-mapViewPosX[0])*atob));
517 mofsy = cast(int)(mapViewPosY[0]+roundf((mapViewPosY[1]-mapViewPosY[0])*atob));
518 } else {
519 mofsx = mapViewPosX[1];
520 mofsy = mapViewPosY[1];
522 vportX0 = mofsx/scale;
523 vportY0 = mofsy/scale;
524 vportX1 = vportX0+vlWidth/scale;
525 vportY1 = vportY0+vlHeight/scale;
528 if (mapTilesChanged != 0) rebuildMapMegaTextures();
530 dotDraw(atob);
532 // build background layer
533 fboOrigBack.exec({
534 //glDisable(GL_BLEND);
535 //glClearDepth(1.0f);
536 //glDepthFunc(GL_LESS);
537 //glDepthFunc(GL_NEVER);
538 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
539 glClear(GL_COLOR_BUFFER_BIT/*|GL_DEPTH_BUFFER_BIT*/);
540 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
541 orthoCamera(map.width*8, map.height*8);
543 glEnable(GL_BLEND);
544 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
545 // draw sky
547 drawAtXY(map.skytexgl.tid, mapOfsX/2-map.MapSize*8, mapOfsY/2-map.MapSize*8, map.MapSize*8, map.MapSize*8);
548 drawAtXY(map.skytexgl.tid, mapOfsX/2-map.MapSize*8, mapOfsY/2, map.MapSize*8, map.MapSize*8);
549 drawAtXY(map.skytexgl.tid, mapOfsX/2, mapOfsY/2-map.MapSize*8, map.MapSize*8, map.MapSize*8);
550 drawAtXY(map.skytexgl.tid, mapOfsX/2, mapOfsY/2, map.MapSize*8, map.MapSize*8);
552 drawAtXY(map.skytexgl.tid, 0, 0, map.MapSize*8, map.MapSize*8);
553 // draw background
554 drawAtXY(map.texgl.ptr[map.Back], 0, 0);
555 // draw distorted liquid areas
556 shadLiquidDistort.exec((Shader shad) {
557 shad["iDistortTime"] = iLiquidTime;
558 drawAtXY(map.texgl.ptr[map.AllLiquids], 0, 0);
560 // monsters, items; we'll do linear interpolation here
561 glColor3f(1.0f, 1.0f, 1.0f);
562 //glEnable(GL_DEPTH_TEST);
563 attachedLightCount = 0;
565 // who cares about memory?!
566 // draw order: players, items, monsters, other
567 static struct DrawInfo {
568 ActorDef adef;
569 ActorId aid;
570 int actorX, actorY;
571 @disable this (this); // no copies
573 enum { Players, Items, Monsters, Other }
574 __gshared DrawInfo[65536][4] drawlists;
575 __gshared uint[4] dlpos;
576 DrawInfo camchickdi;
578 dlpos[] = 0;
579 ActorId camchick;
581 Actor.forEach((ActorId me) {
582 //me.fprop_0drawlistpos = 0;
583 if (auto adef = findActorDef(me)) {
584 uint dlnum = Other;
585 switch (adef.classtype.get) {
586 case "monster": dlnum = (adef.classname.get != "Player" ? Monsters : Players); break;
587 case "item": dlnum = Items; break;
588 default: dlnum = Other; break;
590 int actorX, actorY; // current actor position
592 auto ofs = prevFrameActorOfs.ptr[me.id&0xffff];
593 if (frameInterpolation && ofs < uint.max-1 && Actor.isSameSavedActor(me.id, prevFrameActorsData.ptr, ofs)) {
594 import core.stdc.math : roundf;
595 auto xptr = prevFrameActorsData.ptr+ofs;
596 int ox = xptr.fgetp_x;
597 int nx = me.fget_x;
598 int oy = xptr.fgetp_y;
599 int ny = me.fget_y;
600 actorX = cast(int)(ox+roundf((nx-ox)*atob));
601 actorY = cast(int)(oy+roundf((ny-oy)*atob));
602 //conwriteln("actor ", me.id, "; o=(", ox, ",", oy, "); n=(", nx, ",", ny, "); p=(", x, ",", y, ")");
603 } else {
604 actorX = me.fget_x;
605 actorY = me.fget_y;
608 if (!camchick.valid && me.flags!uint&AF_CAMERACHICK) {
609 camchick = me;
610 camchickdi.adef = adef;
611 camchickdi.aid = me;
612 camchickdi.actorX = actorX;
613 camchickdi.actorY = actorY;
615 // draw sprite
616 if ((me.fget_flags&AF_NODRAW) == 0) {
617 //auto dl = &drawlists[dlnum][dlpos.ptr[dlnum]];
618 //me.fprop_0drawlistpos = (dlpos.ptr[dlnum]&0xffff)|((dlnum&0xff)<<16);
619 auto dl = drawlists.ptr[dlnum].ptr+dlpos.ptr[dlnum];
620 ++dlpos.ptr[dlnum];
621 dl.adef = adef;
622 dl.aid = me;
623 dl.actorX = actorX;
624 dl.actorY = actorY;
626 if (auto isp = adef.animSpr(me.fget_zAnimstate, me.fget_dir, me.fget_zAnimidx)) {
627 drawAtXY(isp.tex, actorX-isp.vga.sx, actorY-isp.vga.sy, adef.zz);
628 } else {
629 //conwriteln("no animation for actor ", me.id, " (", me.classtype!string, ":", me.classname!string, ")");
633 // process attached lights
634 if ((me.fget_flags&AF_NOLIGHT) == 0) {
635 uint alr = me.fget_attLightRGBX;
636 if ((alr&0xff) >= 4) {
637 // yep, add it
638 auto li = attachedLights.ptr+attachedLightCount;
639 ++attachedLightCount;
640 li.x = actorX+me.fget_attLightXOfs;
641 li.y = actorY+me.fget_attLightYOfs;
642 li.r = ((alr>>24)&0xff)/255.0f; // red or intensity
643 if ((alr&0x00_ff_ff_00U) == 0x00_00_01_00U) {
644 li.uncolored = true;
645 } else {
646 li.g = ((alr>>16)&0xff)/255.0f;
647 li.b = ((alr>>8)&0xff)/255.0f;
648 li.uncolored = false;
650 li.radius = (alr&0xff);
651 if (li.radius > MaxLightRadius) li.radius = MaxLightRadius;
654 } else {
655 conwriteln("not found actor ", me.id, " (", me.classtype!string, ":", me.classname!string, ")");
658 // draw actor lists
659 foreach_reverse (uint dlnum; 0..drawlists.length) {
660 auto dl = drawlists.ptr[dlnum].ptr;
661 if (dlnum == Players) dl += dlpos.ptr[dlnum]-1;
662 foreach (uint idx; 0..dlpos.ptr[dlnum]) {
663 auto me = dl.aid;
664 if (auto isp = dl.adef.animSpr(me.fget_zAnimstate, me.fget_dir, me.fget_zAnimidx)) {
665 //drawAtXY(isp.tex, dl.actorX-isp.vga.sx, dl.actorY-isp.vga.sy);
666 isp.drawAtXY(dl.actorX, dl.actorY);
668 if (dlnum != Players) ++dl; else --dl;
671 if (camCenter && camchick.valid) {
672 int tiltHeight = /*getMapViewHeight()*/(vlHeight/scale)/4;
673 int vy = camchick.looky!int;
674 if (vy < -tiltHeight) vy = -tiltHeight; else if (vy > tiltHeight) vy = tiltHeight;
675 int swdt = vlWidth/scale;
676 int shgt = vlHeight/scale;
677 int x = camchickdi.actorX-swdt/2;
678 int y = (camchickdi.actorY+vy)-shgt/2;
679 if (x < 0) x = 0; else if (x >= map.width*8-swdt) x = map.width*8-swdt-1;
680 if (y < 0) y = 0; else if (y >= map.height*8-shgt) y = map.height*8-shgt-1;
681 mofsx = x*2;
682 mofsy = y*2;
683 vportX0 = mofsx/scale;
684 vportY0 = mofsy/scale;
685 vportX1 = vportX0+vlWidth/scale;
686 vportY1 = vportY0+vlHeight/scale;
688 //glDisable(GL_DEPTH_TEST);
689 // draw dots
690 drawAtXY(texParts, 0, 0);
691 // do liquid coloring
692 drawAtXY(map.texgl.ptr[map.LiquidMask], 0, 0);
693 // foreground -- hide secrets, draw lifts and such
694 drawAtXY(map.texgl.ptr[map.Front], 0, 0);
698 if (doLighting) {
699 // clear light layer
700 fboLevelLight.exec({
701 glDisable(GL_BLEND);
702 glClearColor(BackIntens, BackIntens, BackIntens, 1.0f);
703 //glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
704 ////glColor4f(1.0f, 1.0f, 1.0f, 0.0f);
705 glClear(GL_COLOR_BUFFER_BIT);
708 // texture 1 is background
709 glActiveTexture(GL_TEXTURE0+1);
710 glBindTexture(GL_TEXTURE_2D, fboOrigBack.tex.tid);
711 // texture 2 is occluders
712 glActiveTexture(GL_TEXTURE0+2);
713 glBindTexture(GL_TEXTURE_2D, map.texgl.ptr[map.LightMask].tid);
714 // done texture assign
715 glActiveTexture(GL_TEXTURE0+0);
717 enum LYOfs = 1;
719 renderLight( 27, 391-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 100);
720 renderLight(542, 424-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 100);
721 renderLight(377, 368-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 32);
722 renderLight(147, 288-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
723 renderLight( 71, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
724 renderLight(249, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
725 renderLight(426, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
726 renderLight(624, 200-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 128);
727 renderLight(549, 298-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
728 renderLight( 74, 304-0+LYOfs, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 32);
730 renderLight(24*8+4, (24+18)*8-2+LYOfs, SVec4F(0.6f, 0.0f, 0.0f, 1.0f), 128);
732 renderLight(280, 330, SVec4F(0.0f, 0.0f, 0.0f, 1.0f), 64);
734 // attached lights
735 foreach (ref li; attachedLights[0..attachedLightCount]) {
736 if (li.uncolored) {
737 renderLight(li.x, li.y, SVec4F(0.0f, 0.0f, 0.0f, li.r), li.radius);
738 } else {
739 renderLight(li.x, li.y, SVec4F(li.r, li.g, li.b, 1.0f), li.radius);
744 if (testLightMoved) {
745 testLightX = testLightX/scale+mofsx/scale;
746 testLightY = testLightY/scale+mofsy/scale;
747 testLightMoved = false;
749 foreach (immutable _; 0..1) {
750 renderLight(testLightX, testLightY, SVec4F(0.3f, 0.3f, 0.0f, 1.0f), 96);
754 glActiveTexture(GL_TEXTURE0+1);
755 glBindTexture(GL_TEXTURE_2D, 0);
756 glActiveTexture(GL_TEXTURE0+2);
757 glBindTexture(GL_TEXTURE_2D, 0);
758 glActiveTexture(GL_TEXTURE0+0);
761 // draw scaled level
763 shadScanlines.exec((Shader shad) {
764 shad["scanlines"] = scanlines;
765 glClearColor(BackIntens, BackIntens, BackIntens, 1.0f);
766 glClear(GL_COLOR_BUFFER_BIT);
767 orthoCamera(vlWidth, vlHeight);
768 //orthoCamera(map.width*8*scale, map.height*8*scale);
769 //glMatrixMode(GL_MODELVIEW);
770 //glLoadIdentity();
771 //glTranslatef(0.375, 0.375, 0); // to be pixel-perfect
772 // somehow, FBO objects are mirrored; wtf?!
773 drawAtXY(fboLevel.tex.tid, -mapOfsX, -mapOfsY, map.width*8*scale, map.height*8*scale, mirrorY:true);
774 //glLoadIdentity();
779 fboLevelLight.exec({
780 smDrawText(map.width*8/2, map.height*8/2, "Testing...");
785 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
786 glDisable(GL_BLEND);
789 fboOrigBack.exec({
790 //auto img = smfont.ptr[0x39];
791 auto img = fftest;
792 if (img !is null) {
793 //conwriteln("img.sx=", img.sx, "; img.sy=", img.sy, "; ", img.width, "x", img.height);
794 drawAtXY(img.tex, 10-img.sx, 10-img.sy);
801 { // http://stackoverflow.com/questions/7207422/setting-up-opengl-multiple-render-targets
802 GLenum[1] buffers = [ GL_BACK_LEFT, /*GL_COLOR_ATTACHMENT0_EXT*/ ];
803 //GLenum[1] buffers = [ GL_NONE, /*GL_COLOR_ATTACHMENT0_EXT*/ ];
804 glDrawBuffers(1, buffers.ptr);
808 orthoCamera(vlWidth, vlHeight);
809 auto tex = (doLighting ? fboLevelLight.tex.tid : fboOrigBack.tex.tid);
810 //mofsx &= ~1;
811 //mofsy &= ~1;
812 drawAtXY(tex, -mofsx, -mofsy, map.width*8*scale, map.height*8*scale, mirrorY:true);
814 doMessages(curtime);
818 // ////////////////////////////////////////////////////////////////////////// //
819 // returns time slept
820 int sleepAtMaxMsecs (int msecs) {
821 if (msecs > 0) {
822 import core.sys.posix.signal : timespec;
823 import core.sys.posix.time : nanosleep;
824 timespec ts = void, tpassed = void;
825 ts.tv_sec = 0;
826 ts.tv_nsec = msecs*1000*1000+(500*1000); // milli to nano
827 nanosleep(&ts, &tpassed);
828 return (ts.tv_nsec-tpassed.tv_nsec)/(1000*1000);
829 } else {
830 return 0;
835 // ////////////////////////////////////////////////////////////////////////// //
836 // rendering thread
837 shared int diedie = 0;
839 enum D2DFrameTime = 55; // milliseconds
840 enum MinFrameTime = 1000/60; // ~60 FPS
842 public void renderThread (Tid starterTid) {
843 send(starterTid, 42);
844 try {
845 MonoTime curtime = MonoTime.currTime;
847 lastthink = curtime; // for interpolator
848 nextthink = curtime+dur!"msecs"(D2DFrameTime);
849 MonoTime nextvframe = curtime;
851 enum MaxFPSFrames = 16;
852 float frtimes = 0.0f;
853 int framenum = 0;
854 int prevFPS = -1;
855 int hushFrames = 6; // ignore first `hushFrames` frames overtime
856 MonoTime prevFrameStartTime = curtime;
858 bool vframeWasLost = false;
860 void resetFrameTimers () {
861 MonoTime curtime = MonoTime.currTime;
862 lastthink = curtime; // for interpolator
863 nextthink = curtime+dur!"msecs"(D2DFrameTime);
864 nextvframe = curtime;
867 void loadNewLevel (string name) {
869 if (levelLoaded) {
870 conwriteln("ERROR: can't load new levels yet");
871 return;
874 if (name.length == 0) {
875 conwriteln("ERROR: can't load empty level!");
877 conwriteln("loading map '", name, "'");
878 loadMap(name);
879 resetFrameTimers();
882 void receiveMessages () {
883 for (;;) {
884 import core.time : Duration;
885 //conwriteln("rendering thread: waiting for messages...");
886 auto got = receiveTimeout(
887 Duration.zero, // don't wait
888 (TMsgMessage msg) {
889 addMessage(msg.text[0..msg.textlen], msg.pauseMsecs, msg.noreplace);
891 (TMsgLoadLevel msg) {
892 nextmapname = null; // clear "exit" flag
893 loadNewLevel(msg.mapfile[0..msg.textlen].idup);
895 (TMsgSkipLevel msg) {
896 string mn = genNextMapName(0);
897 if (mn.length) {
898 nextmapname = null; // clear "exit" flag
899 loadNewLevel(mn);
902 (TMsgIntOption msg) {
903 final switch (msg.type) with (TMsgIntOption.Type) {
904 case Scale:
905 if (msg.value >= 1 && msg.value <= 2) scale = msg.value;
906 break;
909 (TMsgBoolOption msg) {
910 final switch (msg.type) with (TMsgBoolOption.Type) {
911 case Interpolation:
912 if (msg.toggle) msg.value = !frameInterpolation;
913 if (frameInterpolation != msg.value) {
914 frameInterpolation = msg.value;
915 } else {
916 msg.showMessage = false;
918 break;
919 case Lighting:
920 if (msg.toggle) msg.value = !doLighting;
921 if (doLighting != msg.value) {
922 doLighting = msg.value;
923 } else {
924 msg.showMessage = false;
926 break;
927 case CheatNoDoors:
928 if (msg.toggle) msg.value = !cheatNoDoors;
929 if (cheatNoDoors != msg.value) {
930 cheatNoDoors = msg.value;
931 } else {
932 msg.showMessage = false;
934 break;
935 case CheatNoWallClip:
936 if (msg.toggle) msg.value = !cheatNoWallClip;
937 if (CheatNoWallClip != msg.value) {
938 cheatNoWallClip = msg.value;
939 } else {
940 msg.showMessage = false;
942 break;
944 if (msg.showMessage) {
945 char[128] mbuf;
946 uint msgpos;
947 void putStr(T) (T s) if (is(T : const(char)[])) {
948 foreach (char ch; s) {
949 if (msgpos >= mbuf.length) break;
950 mbuf.ptr[msgpos++] = ch;
953 putStr("Option \"");
954 { import std.conv : to; putStr(to!string(msg.type)); }
955 putStr("\": O");
956 if (msg.value) putStr("N"); else putStr("FF");
957 addMessage(mbuf[0..msgpos]);
960 (TMsgTestLightMove msg) {
961 testLightX = msg.x;
962 testLightY = msg.y;
963 testLightMoved = true;
965 (Variant v) {
966 conwriteln("WARNING: unknown thread message received and ignored");
969 if (!got) {
970 // no more messages
971 //conwriteln("rendering thread: no more messages");
972 break;
975 if (nextmapname.length) {
976 string mn = nextmapname;
977 nextmapname = null; // clear "exit" flag
978 loadNewLevel(mn);
982 // "D2D frames"; curtime should be set; return `true` if frame was processed; will fix `curtime`
983 bool doThinkFrame () {
984 if (curtime >= nextthink) {
985 lastthink = curtime;
986 while (nextthink <= curtime) nextthink += dur!"msecs"(D2DFrameTime);
987 if (levelLoaded) {
988 // save snapshot and other data for interpolator
989 Actor.saveSnapshot(prevFrameActorsData[], prevFrameActorOfs.ptr);
990 mapViewPosX[0] = mapViewPosX[1];
991 mapViewPosY[0] = mapViewPosY[1];
992 // process actors
993 doActorsThink();
994 dotThink();
996 // some timing
997 auto tm = MonoTime.currTime;
998 int thinkTime = cast(int)((tm-curtime).total!"msecs");
999 if (thinkTime > 9) { import core.stdc.stdio; printf("spent on thinking: %d msecs\n", thinkTime); }
1000 curtime = tm;
1001 return true;
1002 } else {
1003 return false;
1007 // "video frames"; curtime should be set; return `true` if frame was processed; will fix `curtime`
1008 bool doVFrame () {
1009 version(dont_use_vsync) {
1010 // timer
1011 enum doCheckTime = true;
1012 } else {
1013 // vsync
1014 __gshared bool prevLost = false;
1015 bool doCheckTime = vframeWasLost;
1016 if (vframeWasLost) {
1017 if (!prevLost) {
1018 { import core.stdc.stdio; printf("frame was lost!\n"); }
1020 prevLost = true;
1021 } else {
1022 prevLost = false;
1025 if (doCheckTime) {
1026 if (curtime < nextvframe) return false;
1027 version(dont_use_vsync) {
1028 if (curtime > nextvframe) {
1029 auto overtime = cast(int)((curtime-nextvframe).total!"msecs");
1030 if (overtime > 2500) {
1031 if (hushFrames) {
1032 --hushFrames;
1033 } else {
1034 { import core.stdc.stdio; printf(" spent whole %d msecs\n", overtime); }
1040 while (nextvframe <= curtime) nextvframe += dur!"msecs"(MinFrameTime);
1041 bool ctset = false;
1043 sdwindow.mtLock();
1044 scope(exit) sdwindow.mtUnlock();
1045 ctset = sdwindow.setAsCurrentOpenGlContextNT;
1047 // if we can't set context, pretend that videoframe was processed; this should solve problem with vsync and invisible window
1048 if (ctset) {
1049 // render scene
1050 iLiquidTime = cast(float)((curtime-MonoTime.zero).total!"msecs"%10000000)/18.0f*0.04f;
1051 receiveMessages(); // here, 'cause we need active OpenGL context for some messages
1052 if (levelLoaded) {
1053 renderScene(curtime);
1054 } else {
1055 //renderLoading(curtime);
1057 sdwindow.mtLock();
1058 scope(exit) sdwindow.mtUnlock();
1059 sdwindow.swapOpenGlBuffers();
1060 glFinish();
1061 sdwindow.releaseCurrentOpenGlContext();
1062 vframeWasLost = false;
1063 } else {
1064 vframeWasLost = true;
1065 { import core.stdc.stdio; printf("xframe was lost!\n"); }
1067 curtime = MonoTime.currTime;
1068 return true;
1071 for (;;) {
1072 if (sdwindow.closed) break;
1073 if (atomicLoad(diedie) > 0) break;
1075 curtime = MonoTime.currTime;
1076 auto fstime = curtime;
1078 doThinkFrame(); // this will fix curtime if necessary
1079 if (doVFrame()) {
1080 if (!vframeWasLost) {
1081 // fps
1082 auto frameTime = cast(float)(curtime-prevFrameStartTime).total!"msecs"/1000.0f;
1083 prevFrameStartTime = curtime;
1084 frtimes += frameTime;
1085 if (++framenum >= MaxFPSFrames || frtimes >= 3.0f) {
1086 import std.string : format;
1087 int newFPS = cast(int)(cast(float)MaxFPSFrames/frtimes+0.5);
1088 if (newFPS != prevFPS) {
1089 sdwindow.title = "%s / FPS:%s".format("D2D", newFPS);
1090 prevFPS = newFPS;
1092 framenum = 0;
1093 frtimes = 0.0f;
1098 curtime = MonoTime.currTime;
1100 // now sleep until next "video" or "think" frame
1101 if (nextthink > curtime && nextvframe > curtime) {
1102 // let's decide
1103 immutable nextVideoFrameSleep = cast(int)((nextvframe-curtime).total!"msecs");
1104 immutable nextThinkFrameSleep = cast(int)((nextthink-curtime).total!"msecs");
1105 immutable sleepTime = (nextVideoFrameSleep < nextThinkFrameSleep ? nextVideoFrameSleep : nextThinkFrameSleep);
1106 sleepAtMaxMsecs(sleepTime);
1107 //curtime = MonoTime.currTime;
1110 } catch (Exception e) {
1111 import core.stdc.stdio;
1112 fprintf(stderr, "FUUUUUUUUUUUUUUUUUUUUUUUUUU\n");
1113 for (;;) {
1114 if (sdwindow.closed) break;
1115 if (atomicLoad(diedie) > 0) break;
1116 sleepAtMaxMsecs(100);
1119 atomicStore(diedie, 2);
1123 // ////////////////////////////////////////////////////////////////////////// //
1124 __gshared Tid renderTid;
1125 shared bool renderThreadStarted = false;
1128 public void startRenderThread () {
1129 if (!cas(&renderThreadStarted, false, true)) {
1130 assert(0, "render thread already started!");
1132 renderTid = spawn(&renderThread, thisTid);
1133 setMaxMailboxSize(renderTid, 1024, OnCrowding.throwException); //FIXME
1134 // wait for "i'm ready" signal
1135 receive(
1136 (int ok) {
1137 if (ok != 42) assert(0, "wtf?!");
1140 conwriteln("rendering thread started");
1144 // ////////////////////////////////////////////////////////////////////////// //
1145 public void closeWindow () {
1146 if (atomicLoad(diedie) != 2) {
1147 atomicStore(diedie, 1);
1148 while (atomicLoad(diedie) != 2) {}
1150 if (!sdwindow.closed) {
1151 flushGui();
1152 sdwindow.close();
1157 // ////////////////////////////////////////////////////////////////////////// //
1158 // thread messages
1161 // ////////////////////////////////////////////////////////////////////////// //
1162 struct TMsgTestLightMove {
1163 int x, y;
1166 public void postTestLightMove (int x, int y) {
1167 if (!atomicLoad(renderThreadStarted)) return;
1168 auto msg = TMsgTestLightMove(x, y);
1169 send(renderTid, msg);
1173 // ////////////////////////////////////////////////////////////////////////// //
1174 struct TMsgMessage {
1175 char[256] text;
1176 uint textlen;
1177 int pauseMsecs;
1178 bool noreplace;
1181 public void postAddMessage (const(char)[] msgtext, int pauseMsecs=3000, bool noreplace=false) {
1182 if (!atomicLoad(renderThreadStarted)) return;
1183 if (msgtext.length > TMsgMessage.text.length) msgtext = msgtext[0..TMsgMessage.text.length];
1184 TMsgMessage msg;
1185 msg.textlen = cast(uint)msgtext.length;
1186 if (msg.textlen) msg.text[0..msg.textlen] = msgtext[0..msg.textlen];
1187 msg.pauseMsecs = pauseMsecs;
1188 msg.noreplace = noreplace;
1189 send(renderTid, msg);
1193 // ////////////////////////////////////////////////////////////////////////// //
1194 struct TMsgLoadLevel {
1195 char[1024] mapfile;
1196 uint textlen;
1199 public void postLoadLevel (const(char)[] mapfile) {
1200 if (!atomicLoad(renderThreadStarted)) return;
1201 if (mapfile.length > TMsgLoadLevel.mapfile.length) mapfile = mapfile[0..TMsgLoadLevel.mapfile.length];
1202 TMsgLoadLevel msg;
1203 msg.textlen = cast(uint)mapfile.length;
1204 if (msg.textlen) msg.mapfile[0..msg.textlen] = mapfile[0..msg.textlen];
1205 send(renderTid, msg);
1209 // ////////////////////////////////////////////////////////////////////////// //
1210 struct TMsgSkipLevel {
1213 public void postSkipLevel () {
1214 if (!atomicLoad(renderThreadStarted)) return;
1215 TMsgSkipLevel msg;
1216 send(renderTid, msg);
1220 // ////////////////////////////////////////////////////////////////////////// //
1221 struct TMsgIntOption {
1222 enum Type {
1223 Scale,
1225 Type type;
1226 int value;
1227 bool showMessage;
1231 struct TMsgBoolOption {
1232 enum Type {
1233 Interpolation,
1234 Lighting,
1235 CheatNoDoors,
1236 CheatNoWallClip,
1238 Type type;
1239 bool value;
1240 bool toggle;
1241 bool showMessage;
1245 bool strCaseEqu (const(char)[] s0, const(char)[] s1) {
1246 if (s0.length != s1.length) return false;
1247 foreach (immutable idx, char ch; s0) {
1248 if (ch >= 'A' && ch <= 'Z') ch += 32;
1249 char c1 = s1.ptr[idx];
1250 if (c1 >= 'A' && c1 <= 'Z') c1 += 32;
1251 if (ch != c1) return false;
1253 return true;
1257 public bool postToggleOption (const(char)[] name, bool showMessage=false) { pragma(inline, true); return postSetOption(name, true, showMessage:showMessage, toggle:true); }
1259 public bool postSetOption(T) (const(char)[] name, T value, bool showMessage=false, bool toggle=false) if ((is(T == int) || is(T == bool))) {
1260 if (!atomicLoad(renderThreadStarted)) return false;
1261 if (name.length == 0 || name.length > 127) return false;
1262 static if (is(T == int)) {
1263 TMsgIntOption msg;
1264 foreach (string oname; __traits(allMembers, TMsgIntOption.Type)) {
1265 if (strCaseEqu(oname, name)) {
1266 msg.type = __traits(getMember, TMsgIntOption.Type, oname);
1267 msg.value = value;
1268 msg.showMessage = showMessage;
1269 send(renderTid, msg);
1270 return true;
1273 } else static if (is(T == bool)) {
1274 TMsgBoolOption msg;
1275 foreach (string oname; __traits(allMembers, TMsgBoolOption.Type)) {
1276 if (strCaseEqu(oname, name)) {
1277 msg.type = __traits(getMember, TMsgBoolOption.Type, oname);
1278 msg.value = value;
1279 msg.toggle = toggle;
1280 msg.showMessage = showMessage;
1281 send(renderTid, msg);
1282 return true;
1285 } else {
1286 static assert(0, "invalid option type '"~T.stringof~"'");
1288 return false;