1 module gfx.decl.engine;
2 
3 import gfx.core.rc : Disposable;
4 import gfx.core.typecons : Option;
5 import gfx.graal.format : Format;
6 import gfx.decl.sdlang;
7 
8 class GfxSDLErrorException : Exception
9 {
10     string msg;
11     Location location;
12 
13     this(string msg, Location location) {
14         import std.format : format;
15         this.msg = msg;
16         this.location = location;
17         super(format("%s:(%s,%s): Gfx-SDL error: %s", location.file, location.line+1, location.col+1, msg));
18     }
19 }
20 
21 class NoSuchAssetException : GfxSDLErrorException
22 {
23     string assetPath;
24 
25     this(string assetPath, Location location=Location.init)
26     {
27         import std.format : format;
28         this.assetPath = assetPath;
29         super(format("no such asset: %s", assetPath), location);
30     }
31 }
32 
33 class NoSuchViewException : GfxSDLErrorException
34 {
35     string viewName;
36 
37     this(string viewName, Location location=Location.init)
38     {
39         import std.format : format;
40         this.viewName = viewName;
41         super(format("no such view: %s", viewName), location);
42     }
43 }
44 
45 class NoSuchStructException : GfxSDLErrorException
46 {
47     string structName;
48 
49     this(string structName, Location location=Location.init)
50     {
51         import std.format : format;
52         this.structName = structName;
53         super(format("no such struct: %s", structName), location);
54     }
55 }
56 
57 class NoSuchFieldException : GfxSDLErrorException
58 {
59     string structName;
60     string fieldName;
61 
62     this(string structName, string fieldName, Location location=Location.init)
63     {
64         import std.format : format;
65         this.structName = structName;
66         this.fieldName = fieldName;
67         super(format("no such field: %s.%s", structName, fieldName), location);
68     }
69 }
70 
71 class UnknownFieldFormatException : GfxSDLErrorException
72 {
73     string structName;
74     string fieldName;
75 
76     this(string structName, string fieldName, Location location=Location.init)
77     {
78         import std.format : format;
79         this.structName = structName;
80         this.fieldName = fieldName;
81         super(format("field format is not known for: %s.%s", structName, fieldName), location);
82     }
83 }
84 
85 class DeclarativeEngine : Disposable
86 {
87     import gfx.core.rc : Rc;
88     import gfx.core.typecons;
89     import gfx.decl.store : DeclarativeStore;
90     import gfx.graal.cmd : Access, PipelineStage;
91     import gfx.graal.device : Device;
92     import gfx.graal.image : ImageLayout;
93     import gfx.graal.pipeline;
94     import gfx.graal.renderpass;
95     import gfx.graal.types;
96     import std.exception : enforce;
97     import std.format : format;
98 
99     private Rc!Device _device;
100     private DeclarativeStore _store;
101     private string[] _assetPaths;
102     private immutable(void)[][string] _views;
103     private StructDecl[] _structDecls;
104     private string[] _localKeys;
105     private int _storeAnonKey;
106 
107 
108     this(Device device) {
109         _device = device;
110         _store = new DeclarativeStore;
111     }
112 
113     override void dispose() {
114         _store.dispose();
115         _store = null;
116         _device.unload();
117     }
118 
119     @property DeclarativeStore store() {
120         return _store;
121     }
122 
123     void addAssetPath(in string path) {
124         _assetPaths ~= path;
125     }
126 
127     void addView(string name)() {
128         _views[name] = cast(immutable(void)[])import(name);
129     }
130 
131     void declareStruct(T)() {
132         _structDecls ~= StructDecl.makeFor!T();
133     }
134 
135     void parseSDLSource(string sdl, string filename=null)
136     {
137         auto root = parseSource(sdl, filename);
138         parseSDL(root);
139     }
140 
141     void parseSDLFile(string filename)
142     {
143         auto root = parseFile(filename);
144         parseSDL(root);
145     }
146 
147     void parseSDLView(string name)()
148     {
149         const sdl = cast(string)import(name);
150         auto root = parseSource(sdl, name);
151         parseSDL(root);
152     }
153 
154 
155     private void parseSDL(Tag root)
156     {
157         PipelineInfo[] plis;
158         string[] plKeys;
159 
160         foreach (t; root.namespaces["graal"].tags) {
161             if (t.name == "RenderPass") {
162                 RenderPass rp;
163                 const key = parseRenderPass(t, rp);
164                 _store.store!RenderPass(key, rp);
165             }
166             else if (t.name == "DescriptorSetLayout") {
167                 DescriptorSetLayout dsl;
168                 const key = parseDescriptorSetLayout(t, dsl);
169                 _store.store!DescriptorSetLayout(key, dsl);
170             }
171             else if (t.name == "PipelineLayout") {
172                 PipelineLayout pll;
173                 const key = parsePipelineLayout(t, pll);
174                 _store.store!PipelineLayout(key, pll);
175             }
176             else if (t.name == "ShaderModule") {
177                 ShaderModule sm;
178                 const key = parseShaderModule(t, sm);
179                 _store.store!ShaderModule(key, sm);
180             }
181             else if (t.name == "Pipeline") {
182                 PipelineInfo pli = void;
183                 plKeys ~= parsePipelineInfo(t, pli);
184                 plis ~= pli;
185             }
186             else if (t.name == "PipelineInfo") {
187                 PipelineInfo pli = void;
188                 const key = parsePipelineInfo(t, pli);
189                 _store.store!PipelineInfo(key, pli);
190             }
191         }
192 
193         if (plis.length) {
194             auto pls = _device.createPipelines(plis);
195             foreach(i, pl; pls) {
196                 _store.store!Pipeline(plKeys[i], pl);
197             }
198         }
199 
200         foreach (k; _localKeys) _store.remove(k);
201     }
202 
203     private string parseRenderPass(Tag tag, out RenderPass rp)
204     {
205         auto attachments = parseAttachmentDescriptions(tag.getTag("attachments"));
206         auto subpasses = parseSubpassDescriptions(tag);
207         auto dependencies = parseSubpassDependencies(tag);
208 
209         rp = _device.createRenderPass(attachments, subpasses, dependencies);
210 
211         return getStoreKey(tag);
212     }
213 
214     private AttachmentDescription[] parseAttachmentDescriptions(Tag tag)
215     {
216         import std.conv : to;
217         import std.typecons : Flag;
218         if(!tag) return [];
219 
220         AttachmentDescription[] res;
221 
222         foreach (adt; tag.tags) {
223             AttachmentDescription ad;
224             ad.samples = cast(uint)adt.getAttribute!int("samples", ad.samples);
225             ad.mayAlias = cast(Flag!"mayAlias")adt.getAttribute!bool("mayAlias", cast(bool)ad.mayAlias);
226             foreach (c; adt.tags) {
227                 if (c.name == "format") {
228                     ad.format = expectLiteralOrStoreValValue!Format(c);
229                 }
230                 else if (c.name == "layout") {
231                     ad.layoutTrans.from = c.expectAttribute!string("from").to!ImageLayout;
232                     ad.layoutTrans.to = c.expectAttribute!string("to").to!ImageLayout;
233                 }
234             }
235             if (adt.name == "color" || adt.name == "depth") {
236                 ad.ops = parseAttachmentOps(adt.expectTag("ops"));
237             }
238             else if (adt.name == "stencil") {
239                 ad.stencilOps = parseAttachmentOps(adt.expectTag("ops"));
240             }
241             else if (adt.name == "depthStencil") {
242                 ad.ops = parseAttachmentOps(adt.expectTag("depthOps"));
243                 ad.stencilOps = parseAttachmentOps(adt.expectTag("stencilOps"));
244             }
245             else {
246                 throw new GfxSDLErrorException("unexpected attachment type: "~adt.name, adt.location);
247             }
248             res ~= ad;
249         }
250         return res;
251     }
252 
253     AttachmentOps parseAttachmentOps(Tag tag)
254     {
255         import std.conv : to;
256         AttachmentOps ops;
257         ops.load = tag.expectAttribute!string("load").to!LoadOp;
258         ops.store = tag.expectAttribute!string("store").to!StoreOp;
259         return ops;
260     }
261 
262     SubpassDescription[] parseSubpassDescriptions(Tag tag)
263     {
264         import std.algorithm : filter;
265 
266         SubpassDescription[] res;
267         foreach(sdt; tag.tags.filter!(t => t.name == "subpass")) {
268             SubpassDescription sd;
269             foreach (t; sdt.tags) {
270                 if (t.name == "input") {
271                     sd.inputs ~= parseAttachmentRef(t);
272                 }
273                 else if (t.name == "color") {
274                     sd.colors ~= parseAttachmentRef(t);
275                 }
276                 else if (t.name == "depthStencil") {
277                     sd.depthStencil = parseAttachmentRef(t);
278                 }
279                 else if (t.name == "preserves") {
280                     import std.algorithm : map;
281                     import std.array : array;
282                     sd.preserves = t.values.map!(v => cast(uint)v.get!int()).array();
283                 }
284             }
285             res ~= sd;
286         }
287         return res;
288     }
289 
290     AttachmentRef parseAttachmentRef(Tag tag)
291     {
292         import std.conv : to;
293         AttachmentRef res=void;
294         res.attachment = cast(uint)tag.expectAttribute!int("attachment");
295         res.layout = tag.expectAttribute!string("layout").to!ImageLayout;
296         return res;
297     }
298 
299     SubpassDependency[] parseSubpassDependencies(Tag tag)
300     {
301         import std.algorithm : filter;
302         import std.conv : to;
303 
304         SubpassDependency[] res;
305         foreach(sdt; tag.tags.filter!(t => t.name == "dependency")) {
306             SubpassDependency sd=void;
307             auto t = sdt.expectTag("subpass");
308             sd.subpass.from = parseSubpassRef(t, "from");
309             sd.subpass.to = parseSubpassRef(t, "to");
310             t = sdt.expectTag("stageMask");
311             sd.stageMask.from = t.expectAttribute!string("from").parseFlags!PipelineStage();
312             sd.stageMask.to = t.expectAttribute!string("to").parseFlags!PipelineStage();
313             t = sdt.expectTag("accessMask");
314             sd.accessMask.from = t.expectAttribute!string("from").parseFlags!Access();
315             sd.accessMask.to = t.expectAttribute!string("to").parseFlags!Access();
316             res ~= sd;
317         }
318         return res;
319     }
320 
321     uint parseSubpassRef(Tag tag, string attrName)
322     {
323         import std.conv : to;
324         int val;
325         string str;
326         if (tryGetAttr!int(tag, attrName, val)) {
327             return cast(uint)val;
328         }
329         if (tryGetAttr!string(tag, attrName, str)) {
330             if (str == "external") {
331                 return subpassExternal;
332             }
333         }
334         throw new GfxSDLErrorException(
335             "could not resolve subpass", tag.location
336         );
337     }
338 
339     private string parseDescriptorSetLayout(Tag tag, out DescriptorSetLayout dsl)
340     {
341         import std.conv : to;
342 
343         PipelineLayoutBinding[] bindings;
344         auto btags = tag.expectTag("bindings");
345         foreach (bt; btags.all.tags) {
346             PipelineLayoutBinding binding;
347             binding.binding = bt.expectValue!int();
348             binding.descriptorType = bt.expectAttribute!string("descriptorType").to!DescriptorType;
349             binding.descriptorCount = bt.expectAttribute!int("descriptorCount");
350             binding.stages = bt.expectAttribute!string("stages").parseFlags!ShaderStage();
351             bindings ~= binding;
352         }
353 
354         dsl = _device.createDescriptorSetLayout(bindings);
355         return getStoreKey(tag);
356     }
357 
358     private string parsePipelineLayout(Tag tag, out PipelineLayout pll)
359     {
360         DescriptorSetLayout[] layouts;
361         auto layoutsTag = tag.getTag("layouts");
362         if (layoutsTag) {
363             foreach (lt; layoutsTag.all.tags) {
364                 layouts ~= parseStoreRefOrLiteral!(DescriptorSetLayout, parseDescriptorSetLayout)(lt);
365             }
366         }
367 
368         PushConstantRange[] ranges;
369         auto rangesTag = tag.getTag("ranges");
370         if (rangesTag) {
371             foreach (rt; rangesTag.all.tags) {
372                 PushConstantRange pcr;
373                 pcr.stages = rt.expectAttribute!string("stages").parseFlags!ShaderStage();
374                 pcr.size = expectSizeOffsetAttr(rt, "size");
375                 pcr.offset = expectSizeOffsetAttr(rt, "offset");
376                 ranges ~= pcr;
377             }
378         }
379 
380         pll = _device.createPipelineLayout(layouts, ranges);
381         return getStoreKey(tag);
382     }
383 
384     private string parseShaderModule(Tag tag, out ShaderModule mod)
385     {
386         immutable(uint)[] src;
387         auto st = tag.expectTag("source");
388         string srcStr;
389         ubyte[] srcData;
390         if (st.tryGetValue!string(srcStr)) {
391             src = getShaderSrcFromString(srcStr, st.location);
392         }
393         else if (st.tryGetValue!(ubyte[])(srcData)) {
394             import std.exception : assumeUnique;
395             src = assumeUnique(cast(uint[])srcData);
396         }
397         else {
398             throw new GfxSDLErrorException(
399                 "ill-formed source tag in graal:ShaderModule", tag.location
400             );
401         }
402 
403         const ep = tag.getTagValue!string("entryPoint", "main");
404 
405         mod = _device.createShaderModule(src, ep);
406         return getStoreKey(tag);
407     }
408 
409     private string parseShaderModuleAttr(Tag tag, out ShaderModule mod)
410     {
411         immutable(uint)[] src;
412         auto srcAttr = enforce(findAttr(tag, "source"), new GfxSDLErrorException(
413             "source attribute is mandatory in graal:ShaderModule", tag.location
414         ));
415         string srcStr;
416         ubyte[] srcData;
417         if (srcAttr.tryGetValue!string(srcStr)) {
418             src = getShaderSrcFromString(srcStr, srcAttr.location);
419         }
420         else if (srcAttr.tryGetValue!(ubyte[])(srcData)) {
421             import std.exception : assumeUnique;
422             src = assumeUnique(cast(uint[])srcData);
423         }
424         else {
425             throw new GfxSDLErrorException(
426                 "ill-formed source attribute in graal:ShaderModule", tag.location
427             );
428         }
429 
430         const ep = tag.getAttribute!string("entryPoint", "main");
431 
432         mod = _device.createShaderModule(src, ep);
433         return getStoreKeyAttr(tag);
434     }
435 
436     immutable(uint)[] getShaderSrcFromString(in string srcStr, in Location location)
437     {
438         import std.algorithm : startsWith;
439 
440         if (srcStr.startsWith("view:")) {
441             const name = srcStr["view:".length .. $];
442             return cast(immutable(uint)[])getView(name, location);
443         }
444         else if (srcStr.startsWith("asset:")) {
445             const name = srcStr["asset:".length .. $];
446             return cast(immutable(uint)[])getAsset(name, location);
447         }
448         else {
449             throw new GfxSDLErrorException(
450                 format("\"%s\" could not lead to a shader source file", srcStr),
451                 location
452             );
453         }
454     }
455 
456     private string parsePipelineInfo(Tag tag, out PipelineInfo plInfo)
457     {
458         foreach (t; tag.expectTag("shaders").tags) {
459             ShaderModule sm;
460             if (t.attributes.length) {
461                 // inline shader
462                 const key = parseShaderModuleAttr(t, sm);
463                 _store.store!ShaderModule(key, sm);
464             }
465             else {
466                 // literal or alreadystored
467                 sm = parseStoreRefOrLiteral!(ShaderModule, parseShaderModule)(t);
468             }
469             switch (t.name) {
470             case "vertex":      plInfo.shaders.vertex = sm; break;
471             case "tessControl": plInfo.shaders.tessControl = sm; break;
472             case "tessEval":    plInfo.shaders.tessEval = sm; break;
473             case "geometry":    plInfo.shaders.geometry = sm; break;
474             case "fragment":    plInfo.shaders.fragment = sm; break;
475             default:
476                 throw new GfxSDLErrorException(
477                     "Unknown shader stage: " ~ t.name, t.location
478                 );
479             }
480         }
481 
482         foreach(t; tag.expectTag("inputBindings").tags) {
483             VertexInputBinding vib;
484             vib.binding = t.expectValue!int();
485             vib.stride = expectSizeOffsetAttr(t, "stride");
486             import std.typecons : Flag;
487             vib.instanced = cast(Flag!"instanced")t.getAttribute!bool("instanced", false);
488             plInfo.inputBindings ~= vib;
489         }
490 
491         foreach(t; tag.expectTag("inputAttribs").tags) {
492             VertexInputAttrib via;
493             via.location = t.expectValue!int();
494             via.binding = t.expectAttribute!int("binding");
495             auto attr = t.findAttr("member");
496             if (attr) {
497                 import std.algorithm : findSplit;
498                 const ms = attr.value.get!string();
499                 const sf = ms.findSplit(".");
500                 if (!sf[2].length) {
501                     throw new GfxSDLErrorException(format(
502                         "could not resolve \"%s\" to a struct field", ms
503                     ), attr.location);
504                 }
505                 const s = findStruct(sf[0], attr.location);
506                 via.format = s.getFormat(sf[2], attr.location);
507                 via.offset = s.getOffset(sf[2], attr.location);
508             }
509             else {
510                 via.format = expectFormatAttr(t, "format");
511                 via.offset = expectSizeOffsetAttr(t, "offset");
512             }
513             plInfo.inputAttribs ~= via;
514         }
515 
516         plInfo.assembly = parseInputAssembly(tag.expectTag("assembly"));
517         plInfo.rasterizer = parseRasterizer(tag.expectTag("rasterizer"));
518         plInfo.viewports = parseViewportConfigs(tag);
519         plInfo.depthInfo = parseDepthInfo(tag.getTag("depthInfo"));
520         plInfo.stencilInfo = parseStencilInfo(tag.getTag("stencilInfo"));
521         plInfo.blendInfo = parseColorBlendInfo(tag.expectTag("blendInfo"));
522         auto dynTag = tag.getTag("dynamicStates");
523         if (dynTag) {
524             import std.algorithm : map;
525             import std.array : array;
526             import std.conv : to;
527             plInfo.dynamicStates = dynTag.values.map!(v => v.get!string.to!DynamicState).array;
528         }
529         plInfo.layout = parseStoreRefOrLiteral!(PipelineLayout, parsePipelineLayout)(tag.expectTag("layout"));
530         auto rpTag = tag.expectTag("renderPass");
531         plInfo.renderPass = expectStoreRef!RenderPass(rpTag);
532         plInfo.subpassIndex = rpTag.expectAttribute!int("subpass");
533 
534         return getStoreKey(tag);
535     }
536 
537     private InputAssembly parseInputAssembly(Tag tag)
538     {
539         import std.conv : to;
540         import std.typecons : Flag;
541 
542         InputAssembly assembly;
543         assembly.primitive = tag.expectAttribute!string("primitive").to!Primitive;
544         assembly.primitiveRestart =
545                 cast(Flag!"primitiveRestart")tag.getAttribute!bool("primitiveRestart", false);
546         return assembly;
547     }
548 
549     private Rasterizer parseRasterizer(Tag tag)
550     {
551         import std.conv : to;
552         import std.typecons : Flag;
553         import gfx.core.typecons : some;
554 
555         Rasterizer res;
556         res.mode = tag.expectAttribute!string("polygonMode").to!PolygonMode;
557         res.cull = tag.getAttribute!string("cull", "none").parseFlags!Cull;
558         res.front = tag.getAttribute!string("front", "ccw").to!FrontFace;
559         res.depthClamp = cast(Flag!"depthClamp")tag.getAttribute!bool("depthClamp", false);
560         res.lineWidth = tag.getAttribute!float("lineWidth", 1f);
561         auto dbt = tag.getTag("depthBias");
562         if (dbt) {
563             DepthBias db;
564             db.slopeFactor = dbt.expectAttribute!float("slope");
565             db.constantFactor = dbt.expectAttribute!float("const");
566             db.clamp = dbt.getAttribute!float("clamp", 0f);
567             res.depthBias = some(db);
568         }
569         return res;
570     }
571 
572     private ViewportConfig[] parseViewportConfigs(Tag plTag)
573     {
574         Viewport[] vps;
575         Rect[] scs;
576         Location loc;
577         auto vpt = plTag.getTag("viewports");
578         if (vpt) {
579             foreach(t; plTag.tags) {
580                 if (t.name == "viewport") {
581                     vps ~= parseViewport(t);
582                 }
583                 else if (t.name == "scissors") {
584                     scs ~= parseRect(t);
585                 }
586             }
587             loc = vpt.location;
588         }
589         else {
590             vpt = plTag.getTag("viewport");
591             if (vpt) {
592                 vps ~= parseViewport(vpt);
593                 loc = vpt.location;
594             }
595         }
596 
597         if (!vps.length && !scs.length) return null;
598         if (scs.length && scs.length != vps.length) {
599             throw new GfxSDLErrorException("must state the same number of viewport and scissors", loc);
600         }
601 
602         ViewportConfig[] configs;
603         if (vps.length == scs.length) {
604             foreach (i; 0 .. vps.length) {
605                 configs ~= ViewportConfig(vps[i], scs[i]);
606             }
607         }
608         else {
609             assert(vps.length && !scs.length);
610             foreach (vp; vps) {
611                 const sc = Rect(cast(uint)vp.x, cast(uint)vp.y, cast(uint)vp.width, cast(uint)vp.height);
612                 configs ~= ViewportConfig(vp, sc);
613             }
614         }
615         return configs;
616     }
617 
618 
619     private Viewport parseViewport(Tag tag)
620     {
621         Viewport vp = void;
622         vp.x = expectLiteralOrStoreValAttr!float(tag, "x");
623         vp.y = expectLiteralOrStoreValAttr!float(tag, "y");
624         vp.width = expectLiteralOrStoreValAttr!float(tag, "width");
625         vp.height = expectLiteralOrStoreValAttr!float(tag, "height");
626         vp.minDepth = getLiteralOrStoreValAttr!float(tag, "minDepth", 0f);
627         vp.minDepth = getLiteralOrStoreValAttr!float(tag, "maxDepth", 1f);
628         return vp;
629     }
630 
631     private Rect parseRect(Tag tag)
632     {
633         Rect r = void;
634         r.x = expectLiteralOrStoreValAttr!int(tag, "x");
635         r.y = expectLiteralOrStoreValAttr!int(tag, "y");
636         r.width = expectLiteralOrStoreValAttr!int(tag, "width");
637         r.height = expectLiteralOrStoreValAttr!int(tag, "height");
638         return r;
639     }
640 
641     private DepthInfo parseDepthInfo(Tag tag)
642     {
643         import std.conv : to;
644         import std.typecons : Flag;
645 
646         DepthInfo res;
647         // no tag = disabled = init
648         if (tag) {
649             // can optionally specify on/off as value (defaults to on)
650             res.enabled = cast(Flag!"enabled")tag.getValue!bool(true);
651             if (res.enabled) {
652                 res.write = cast(Flag!"write")tag.expectAttribute!bool("write");
653                 res.compareOp = tag.expectAttribute!string("compareOp").to!CompareOp;
654                 auto bt = tag.getTag("boundsTest");
655                 if (bt) {
656                     // same as depth test, on/off is optional and defaults to on
657                     res.boundsTest = cast(Flag!"boundsTest")tag.getValue!bool(true);
658                     if (res.boundsTest) {
659                         res.minBounds = expectLiteralOrStoreValAttr!float(bt, "min");
660                         res.maxBounds = expectLiteralOrStoreValAttr!float(bt, "max");
661                     }
662                 }
663             }
664         }
665         return res;
666     }
667 
668     StencilInfo parseStencilInfo(Tag tag)
669     {
670         import std.typecons : Flag;
671 
672         StencilInfo res;
673         if (tag) {
674             res.enabled = cast(Flag!"enabled")tag.getValue!bool(true);
675             res.front = parseStencilOpState(tag.getTag("front"));
676             res.back = parseStencilOpState(tag.getTag("back"));
677         }
678         return res;
679     }
680 
681     StencilOpState parseStencilOpState(Tag tag)
682     {
683         import std.conv : to;
684 
685         if (!tag) return StencilOpState.init;
686 
687         StencilOpState res = void;
688         res.compareOp = tag.expectAttribute!string("compareOp").to!CompareOp;
689         string ops;
690         if (tag.tryGetAttr!string("op", ops)) {
691             const op = ops.to!StencilOp;
692             res.failOp = op; res.passOp = op; res.depthFailOp = op;
693         }
694         else {
695             res.failOp = tag.expectAttribute!string("failOp").to!StencilOp;
696             res.passOp = tag.expectAttribute!string("passOp").to!StencilOp;
697             res.depthFailOp = tag.expectAttribute!string("depthFailOp").to!StencilOp;
698         }
699         int mask;
700         if (tag.tryGetAttr!int("mask", mask)) {
701             res.compareMask = mask; res.writeMask = mask; res.refMask = mask;
702         }
703         else {
704             res.compareMask = tag.expectAttribute!int("compareMask");
705             res.writeMask = tag.expectAttribute!int("writeMask");
706             res.refMask = tag.expectAttribute!int("refMask");
707         }
708         return res;
709     }
710 
711     private ColorBlendInfo parseColorBlendInfo(Tag tag)
712     {
713         import gfx.core.typecons : some;
714         import std.conv : to;
715 
716         ColorBlendInfo res;
717 
718         bool lopEnabled;
719         string lopStr;
720         if (tag.tryGetAttr!bool("logicOp", lopEnabled)) {
721             if (lopEnabled) {
722                 throw new GfxSDLErrorException("logicOp can only be set to off or to LogicOp value", tag.location);
723             }
724         }
725         else if (tag.tryGetAttr!string("logicOp", lopStr)) {
726             res.logicOp = some(lopStr.to!LogicOp);
727         }
728 
729         auto attTag = tag.getTag("attachments");
730         if (attTag) foreach(t; attTag.tags) {
731             ColorBlendAttachment attachment;
732             if (t.name == "blend") {
733                 import std.typecons : Yes;
734                 attachment.enabled = Yes.enabled;
735                 auto stateT = t.getTag("state");
736                 if (stateT) {
737                     const state = parseBlendState(stateT);
738                     attachment.colorBlend = state;
739                     attachment.alphaBlend = state;
740                 }
741                 else {
742                     attachment.colorBlend = parseBlendState(t.expectTag("color"));
743                     attachment.alphaBlend = parseBlendState(t.expectTag("alpha"));
744                 }
745             }
746             else if (t.name != "solid") {
747                 throw new GfxSDLErrorException(format(
748                     "tag \"%s\" is not allowed in color attachments", t.name
749                 ), t.location);
750             }
751             string maskStr;
752             if (t.tryGetAttr!string("colorMask", maskStr)) {
753                 attachment.colorMask = maskStr.to!ColorMask;
754             }
755             else {
756                 attachment.colorMask = ColorMask.all;
757             }
758             res.attachments ~= attachment;
759         }
760 
761         auto constTag = tag.getTag("blendConstants");
762         if (constTag) {
763             import std.algorithm : map;
764             import std.array : array;
765 
766             const bc = constTag.values.map!(v => v.coerce!float).array;
767             if (bc.length != 4) {
768                 throw new GfxSDLErrorException("blendConstants must have 4 floats", constTag.location);
769             }
770             res.blendConstants = bc;
771         }
772 
773         return res;
774     }
775 
776     private BlendState parseBlendState(Tag tag)
777     {
778         import std.conv : to;
779 
780         BlendState state;
781         state.factor.from = tag.expectAttribute!string("srcFactor").to!BlendFactor;
782         state.factor.to = tag.expectAttribute!string("dstFactor").to!BlendFactor;
783         state.op = tag.expectAttribute!string("op").to!BlendOp;
784         return state;
785     }
786 
787     private string getStoreKey(Tag tag)
788     {
789         auto t = tag.getTag("store");
790         if (t) return t.expectValue!string();
791         t = tag.getTag("local-store");
792         if (t) {
793             const key = t.expectValue!string();
794             _localKeys ~= key;
795             return key;
796         }
797         else {
798             const key = newAnonKey();
799             _localKeys ~= key;
800             return key;
801         }
802     }
803 
804     private string getStoreKeyAttr(Tag tag)
805     {
806         foreach (attr; tag.attributes) {
807             if (attr.name == "store") {
808                 return attr.value.get!string();
809             }
810             if (attr.name == "local-store") {
811                 const key = attr.value.get!string();
812                 _localKeys ~= key;
813                 return key;
814             }
815         }
816         {
817             const key = newAnonKey();
818             _localKeys ~= key;
819             return key;
820         }
821     }
822 
823     private string newAnonKey()
824     {
825         import std.conv : to;
826         const key = ++_storeAnonKey;
827         return key.to!string;
828     }
829 
830     private immutable(void)[] getAsset(in string name, in Location location)
831     {
832         import std.algorithm : map;
833         import std.file : exists, read;
834         import std.path : buildPath;
835         import std.exception : assumeUnique;
836 
837         foreach (p; _assetPaths.map!(ap => buildPath(ap, name))) {
838             if (exists(p)) {
839                 return assumeUnique(read(p));
840             }
841         }
842         throw new NoSuchAssetException(name, location);
843     }
844 
845     private immutable(void)[] getView(in string name, in Location location)
846     {
847         auto p = name in _views;
848         if (!p) {
849             throw new NoSuchViewException(name, location);
850         }
851         return *p;
852     }
853 
854     private T expectStoreRef(T)(Tag tag)
855     {
856         import std.algorithm : startsWith;
857 
858         string storeStr = tag.expectValue!string();
859         if (!storeStr.startsWith("store:")) {
860             throw new GfxSDLErrorException(
861                 format("Cannot parse %s: " ~
862                     "must specify a store reference or a literal " ~
863                     "(\"%s\" is not a valid store reference)", T.stringof, storeStr),
864                 tag.location
865             );
866         }
867         const key = storeStr["store:".length .. $];
868         return _store.expect!T(key);
869     }
870 
871     private T parseStoreRefOrLiteral(T, alias parseF)(Tag tag)
872     {
873         import std.format : format;
874 
875         string storeStr = tag.getValue!string();
876         const hasChildren = tag.all.tags.length > 0;
877 
878         if (storeStr && hasChildren) {
879             throw new GfxSDLErrorException(
880                 format("Cannot parse %s: both value and children specified", T.stringof),
881                 tag.location
882             );
883         }
884         if (!storeStr && !hasChildren) {
885             // likely value was not a string, or only attributes specified
886             throw new GfxSDLErrorException(
887                 format("Cannot parse %s", T.stringof), tag.location
888             );
889         }
890         T res;
891         if (storeStr) {
892             import std.algorithm : startsWith;
893             if (!storeStr.startsWith("store:")) {
894                 throw new GfxSDLErrorException(
895                     format("Cannot parse %s: " ~
896                         "must specify a store reference or a literal " ~
897                         "(\"%s\" is not a valid store reference)", T.stringof, storeStr),
898                     tag.location
899                 );
900             }
901             const key = storeStr["store:".length .. $];
902             res = _store.expect!T(key);
903         }
904         else {
905             const key = parseF(tag, res);
906             _store.store(key, res);
907         }
908         return enforce(res);
909     }
910 
911     private T getLiteralOrStoreValValue(T)(Tag tag, T def=T.init)
912     {
913         T res;
914         if (!literalOrStoreValValue!T(tag, res)) {
915             res = def;
916         }
917         return res;
918     }
919 
920     private T expectLiteralOrStoreValValue(T)(Tag tag)
921     {
922         T res;
923         if (!literalOrStoreValValue!T(tag, res)) {
924             throw new GfxSDLErrorException(
925                 format("Could not resolve %s value", T.stringof),
926                 tag.location
927             );
928         }
929         return res;
930     }
931 
932     private bool literalOrStoreValValue(T)(Tag tag, out T res)
933     {
934         import std.algorithm : startsWith;
935 
936         static if (!is(T == enum)) {
937             if (tryGetValue(tag, res)) {
938                 return true;
939             }
940         }
941 
942         string str;
943         if (tryGetValue(tag, str)) {
944             if (str.startsWith("store:")) {
945                 const key = str["store:".length .. $];
946                 res = _store.expect!T(key);
947                 return true;
948             }
949             static if (is(T == enum)) {
950                 import std.conv : to;
951                 res = str.to!T;
952                 return true;
953             }
954         }
955         return false;
956     }
957 
958     private T getLiteralOrStoreValAttr(T)(Tag tag, in string attrName, T def=T.init)
959     {
960         T res;
961         if (!literalOrStoreValAttr!T(tag, attrName, res)) {
962             res = def;
963         }
964         return res;
965     }
966 
967     private T expectLiteralOrStoreValAttr(T)(Tag tag, in string attrName)
968     {
969         T res;
970         if (!literalOrStoreValAttr!T(tag, attrName, res)) {
971             throw new GfxSDLErrorException(
972                 format("Could not resolveValue %s value from attribute \"%s\"", T.stringof, attrName),
973                 tag.location
974             );
975         }
976         return res;
977     }
978 
979     private bool literalOrStoreValAttr(T)(Tag tag, in string attrName, out T res)
980     {
981         import std.algorithm : startsWith;
982 
983         static if (!is(T == enum)) {
984             if (tag.tryGetAttr!T(attrName, res)) {
985                 return true;
986             }
987         }
988 
989         string str;
990         if (tryGetAttr(tag, attrName, str)) {
991             if (str.startsWith("store:")) {
992                 const key = str["store:".length .. $];
993                 res = _store.expect!T(key);
994                 return true;
995             }
996             static if (is(T == enum)) {
997                 import std.conv : to;
998                 return str.to!T;
999             }
1000         }
1001         return false;
1002     }
1003 
1004     private T getLiteralOrStoreValTagValue(T)(Tag tag, in string tagName, T def=T.init)
1005     {
1006         T res;
1007         if (!literalOrStoreValTagValue!T(tag, tagName, res)) {
1008             res = def;
1009         }
1010         return res;
1011     }
1012 
1013     private T expectLiteralOrStoreValTagValue(T)(Tag tag, in string tagName)
1014     {
1015         T res;
1016         if (!literalOrStoreValTagValue!T(tag, tagName, res)) {
1017             throw new GfxSDLErrorException(
1018                 format("Could not resolveValue %s value from tag \"%s\"", T.stringof, tagName),
1019                 tag.location
1020             );
1021         }
1022         return res;
1023     }
1024 
1025 
1026     private bool literalOrStoreValTagValue(T)(Tag tag, in string tagName, out T res)
1027     {
1028         import std.algorithm : startsWith;
1029 
1030         static if (!is(T == enum)) {
1031             if (tag.tryGetTagValue!T(tagName, res)) {
1032                 return true;
1033             }
1034         }
1035 
1036         string str;
1037         if (tryGetTagValue(tag, tagName, str)) {
1038             if (str.startsWith("store:")) {
1039                 const key = str["store:".length .. $];
1040                 res = _store.expect!T(key);
1041                 return true;
1042             }
1043             static if (is(T == enum)) {
1044                 import std.conv : to;
1045                 res = str.to!T;
1046                 return true;
1047             }
1048         }
1049         return false;
1050     }
1051 
1052     private uint expectSizeOffsetAttr(Tag tag, in string attrName)
1053     {
1054         auto attr = tag.findAttr(attrName);
1055         if (!attr) {
1056             throw new GfxSDLErrorException(
1057                 "Could not find attribute "~attrName, tag.location
1058             );
1059         }
1060 
1061         if (attr.value.convertsTo!int) {
1062             return attr.value.get!int();
1063         }
1064 
1065         import std.algorithm : findSplit, startsWith;
1066         string s = attr.value.get!string();
1067         if (s.startsWith("sizeof:")) {
1068             const sf = s["sizeof:".length .. $].findSplit(".");
1069             const struct_ = sf[0];
1070             const field = sf[2];
1071             const sd = findStruct(struct_, attr.location);
1072             if (field.length) {
1073                 return cast(uint)sd.getSize(field, attr.location);
1074             }
1075             else {
1076                 return cast(uint)sd.size;
1077             }
1078         }
1079         if (s.startsWith("offsetof:")) {
1080             const sf = s["offsetof:".length .. $].findSplit(".");
1081             const struct_ = sf[0];
1082             const field = sf[2];
1083             if (!field.length) {
1084                 throw new GfxSDLErrorException(
1085                     format("\"%s\" cannot resolve to a struct field (required by \"offsetof:\"", sf),
1086                     attr.location
1087                 );
1088             }
1089             return cast(uint)findStruct(struct_, attr.location).getOffset(field, attr.location);
1090         }
1091         throw new GfxSDLErrorException(
1092             "Could not find attribute "~attrName, tag.location
1093         );
1094     }
1095 
1096 
1097     private Format expectFormatAttr(Tag tag, in string attrName="format")
1098     {
1099         import std.algorithm : findSplit, startsWith;
1100 
1101         auto formatStr = tag.expectAttribute!string(attrName);
1102         if (formatStr.startsWith("formatof:")) {
1103             const sf = formatStr["formatof:".length .. $].findSplit(".");
1104             const struct_ = sf[0];
1105             const field = sf[2];
1106             if (!field.length) {
1107                 throw new GfxSDLErrorException(
1108                     format("\"%s\" cannot resolve to a struct field (required by \"formatof:\"", sf),
1109                     tag.location
1110                 );
1111             }
1112             return findStruct(struct_, tag.location).getFormat(field, tag.location);
1113         }
1114         else {
1115             import std.conv : to;
1116             return formatStr.to!Format;
1117         }
1118     }
1119 
1120     private StructDecl findStruct(in string name, Location location) {
1121         foreach (ref sd; _structDecls) {
1122             if (sd.name == name) return sd;
1123         }
1124         throw new NoSuchStructException(name, location);
1125     }
1126 }
1127 
1128 private:
1129 
1130 Attribute findAttr(Tag tag, string attrName)
1131 {
1132     foreach (attr; tag.attributes)
1133         if (attr.name == attrName) return attr;
1134     return null;
1135 }
1136 
1137 bool tryGetValue(T)(Tag tag, out T val)
1138 {
1139     foreach (ref v; tag.values) {
1140         if (v.convertsTo!T) {
1141             val = v.get!T();
1142             return true;
1143         }
1144     }
1145     return false;
1146 }
1147 
1148 bool tryGetValue(T)(Attribute attr, out T val)
1149 {
1150     if (attr.value.convertsTo!T) {
1151         val = attr.value.get!T();
1152         return true;
1153     }
1154     return false;
1155 }
1156 
1157 bool tryGetAttr(T)(Tag tag, in string name, out T val)
1158 {
1159     foreach (attr; tag.attributes) {
1160         if (attr.name == name) {
1161             if (attr.value.convertsTo!T) {
1162                 val = attr.value.get!T();
1163                 return true;
1164             }
1165         }
1166     }
1167     return false;
1168 }
1169 
1170 bool tryGetTagValue(T)(Tag tag, in string name, out T val)
1171 {
1172     foreach (t; tag.tags) {
1173         if (t.name == name) {
1174             foreach (v; t.values) {
1175                 if (v.convertsTo!T) {
1176                     val = v.get!T();
1177                     return true;
1178                 }
1179             }
1180         }
1181     }
1182     return false;
1183 }
1184 
1185 E parseFlags(E)(in string s) if (is(E == enum))
1186 {
1187     import std.array : split;
1188     import std..string : strip;
1189     import std.conv : to;
1190 
1191     const fl = s.split('|');
1192     E res = cast(E)0;
1193     foreach (f; fl) {
1194         res |= f.strip().to!E;
1195     }
1196     return res;
1197 }
1198 
1199 
1200 struct FieldDecl
1201 {
1202     string name;
1203     size_t size;
1204     size_t offset;
1205     Option!Format format;
1206 }
1207 
1208 struct StructDecl
1209 {
1210     string name;
1211     size_t size;
1212     FieldDecl[] fields;
1213 
1214     static StructDecl makeFor(T)()
1215     {
1216         import std.traits : FieldNameTuple, Fields, getUDAs;
1217 
1218         alias F = Fields!T;
1219         alias N = FieldNameTuple!T;
1220 
1221         const offsets = T.init.tupleof.offsetof;
1222         StructDecl sd;
1223         sd.name = T.stringof;
1224         sd.size = T.sizeof;
1225         foreach(i, FT; F) {
1226             FieldDecl fd;
1227             fd.name = N[i];
1228             fd.size = FT.sizeof;
1229             fd.offset = offsets[i];
1230 
1231             auto udas = getUDAs!(__traits(getMember, T, N[i]), Format);
1232             static if (udas.length) {
1233                 fd.format = udas[0];
1234             }
1235             else {
1236                 fd.format = inferFieldFormat!FT();
1237             }
1238 
1239             sd.fields ~= fd;
1240         }
1241         return sd;
1242     }
1243 
1244     size_t getSize(in string field, in Location location=Location.init) const
1245     {
1246         foreach(f; fields) {
1247             if (f.name == field)
1248                 return f.size;
1249         }
1250         throw new NoSuchFieldException(name, field, location);
1251     }
1252 
1253     size_t getOffset(in string field, in Location location=Location.init) const
1254     {
1255         foreach(f; fields) {
1256             if (f.name == field)
1257                 return f.offset;
1258         }
1259         throw new NoSuchFieldException(name, field, location);
1260     }
1261 
1262     Format getFormat(in string field, in Location location=Location.init) const
1263     {
1264         foreach(f; fields) {
1265             if (f.name == field) {
1266                 if (f.format.isSome) {
1267                     return f.format.get;
1268                 }
1269                 else {
1270                     throw new UnknownFieldFormatException(name, field, location);
1271                 }
1272             }
1273         }
1274         throw new NoSuchFieldException(name, field, location);
1275     }
1276 }
1277 
1278 ///
1279 version(unittest)
1280 {
1281     struct SomeVecType
1282     {
1283         private float[3] rep;
1284     }
1285 
1286     struct SomeVertexType
1287     {
1288         SomeVecType pos;
1289 
1290         float[3] normal;
1291 
1292         @(Format.rgba32_sFloat)
1293         float[4] color;
1294 
1295         @(Format.rgba8_uInt)
1296         ubyte[4] otherField;
1297 
1298         ubyte[4] yetAnotherField;
1299 
1300         @(Format.rgba16_sFloat)
1301         float[3] problemField;
1302     }
1303 
1304     unittest {
1305         const sd = StructDecl.makeFor!SomeVertexType();
1306         assert(sd.name == "SomeVertexType");
1307         assert(sd.size == SomeVertexType.sizeof);
1308         assert(sd.fields[0].name == "pos");
1309         assert(sd.fields[1].name == "normal");
1310         assert(sd.fields[2].name == "color");
1311         assert(sd.fields[3].name == "otherField");
1312         assert(sd.fields[4].name == "yetAnotherField");
1313         assert(sd.fields[5].name == "problemField");
1314 
1315         assert(sd.getSize("pos") == SomeVertexType.pos.sizeof);
1316         assert(sd.getSize("normal") == SomeVertexType.normal.sizeof);
1317         assert(sd.getSize("color") == SomeVertexType.color.sizeof);
1318         assert(sd.getSize("otherField") == SomeVertexType.otherField.sizeof);
1319         assert(sd.getSize("yetAnotherField") == SomeVertexType.yetAnotherField.sizeof);
1320         assert(sd.getSize("problemField") == SomeVertexType.problemField.sizeof);
1321 
1322         assert(sd.getOffset("pos") == SomeVertexType.pos.offsetof);
1323         assert(sd.getOffset("normal") == SomeVertexType.normal.offsetof);
1324         assert(sd.getOffset("color") == SomeVertexType.color.offsetof);
1325         assert(sd.getOffset("otherField") == SomeVertexType.otherField.offsetof);
1326         assert(sd.getOffset("yetAnotherField") == SomeVertexType.yetAnotherField.offsetof);
1327         assert(sd.getOffset("problemField") == SomeVertexType.problemField.offsetof);
1328 
1329         assert(sd.getFormat("pos") == Format.rgb32_sFloat);
1330         assert(sd.getFormat("normal") == Format.rgb32_sFloat);
1331         assert(sd.getFormat("color") == Format.rgba32_sFloat);
1332         assert(sd.getFormat("otherField") == Format.rgba8_uInt);
1333         assert(sd.getFormat("problemField") == Format.rgba16_sFloat);
1334 
1335         import std.exception : assertThrown;
1336         assertThrown!NoSuchFieldException(sd.getSize("jfidjf"));
1337         assertThrown!UnknownFieldFormatException(sd.getFormat("yetAnotherField"));
1338     }
1339 }
1340 
1341 Option!Format inferFieldFormat(T)()
1342 {
1343     import gfx.core.typecons : none, some;
1344     import std.traits : RepresentationTypeTuple;
1345 
1346     alias rtt = RepresentationTypeTuple!T;
1347 
1348     static if (is(T == float)) {
1349         return some(Format.r32_sFloat);
1350     }
1351     else static if (is(T V : V[N], int N)) {
1352         static if (is(V == float) && N == 1) {
1353             return some(Format.r32_sFloat);
1354         }
1355         else static if (is(V == float) && N == 2) {
1356             return some(Format.rg32_sFloat);
1357         }
1358         else static if (is(V == float) && N == 3) {
1359             return some(Format.rgb32_sFloat);
1360         }
1361         else static if (is(V == float) && N == 4) {
1362             return some(Format.rgba32_sFloat);
1363         }
1364         else {
1365             return none!Format;
1366         }
1367     }
1368     else static if (rtt.length == 1 && !is(rtt[0] == T)) {
1369         return inferFieldFormat!(rtt[0])();
1370     }
1371     else {
1372         return none!Format;
1373     }
1374 }