1 #include "triangle_renderer.hpp"
2 #include "canvas_gl.hpp"
3 #include "gl_util.hpp"
4 #include <glm/gtc/type_ptr.hpp>
5 #include "bitmap_font_util.hpp"
6 #include <cmath> // for std::isnan()
7 
8 namespace horizon {
9 
create_vao(GLuint program,GLuint & vbo_out,GLuint & ebo_out)10 static GLuint create_vao(GLuint program, GLuint &vbo_out, GLuint &ebo_out)
11 {
12     GLuint p0_index = 0;
13     GLuint p1_index = 1;
14     GLuint p2_index = 2;
15     GLuint color_index = 3;
16     GLuint lod_index = 4;
17     GLuint color2_index = 5;
18     GL_CHECK_ERROR;
19     GLuint vao, buffer, ebuffer;
20 
21     glGenBuffers(1, &ebuffer);
22     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebuffer);
23 
24     /* we need to create a VAO to store the other buffers */
25     glGenVertexArrays(1, &vao);
26     glBindVertexArray(vao);
27 
28     /* this is the VBO that holds the vertex data */
29     glGenBuffers(1, &buffer);
30     glBindBuffer(GL_ARRAY_BUFFER, buffer);
31     // data is buffered lateron
32 
33     GLfloat vertices[] = {//   Position
34                           0, 0, 7500000, 5000000, 2500000, -2500000, 1, 0, 1};
35     glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
36 
37     /* enable and set the position attribute */
38     glEnableVertexAttribArray(p0_index);
39     glVertexAttribPointer(p0_index, 2, GL_FLOAT, GL_FALSE, sizeof(Triangle), (void *)offsetof(Triangle, x0));
40     glEnableVertexAttribArray(p1_index);
41     glVertexAttribPointer(p1_index, 2, GL_FLOAT, GL_FALSE, sizeof(Triangle), (void *)offsetof(Triangle, x1));
42     glEnableVertexAttribArray(p2_index);
43     glVertexAttribPointer(p2_index, 2, GL_FLOAT, GL_FALSE, sizeof(Triangle), (void *)offsetof(Triangle, x2));
44     glEnableVertexAttribArray(color_index);
45     glVertexAttribIPointer(color_index, 1, GL_UNSIGNED_BYTE, sizeof(Triangle), (void *)offsetof(Triangle, color));
46     glEnableVertexAttribArray(lod_index);
47     glVertexAttribIPointer(lod_index, 1, GL_UNSIGNED_BYTE, sizeof(Triangle), (void *)offsetof(Triangle, lod));
48     glEnableVertexAttribArray(color2_index);
49     glVertexAttribIPointer(color2_index, 1, GL_UNSIGNED_BYTE, sizeof(Triangle), (void *)offsetof(Triangle, color2));
50 
51     GL_CHECK_ERROR;
52 
53     /* enable and set the color attribute */
54     /* reset the state; we will re-enable the VAO when needed */
55     glBindBuffer(GL_ARRAY_BUFFER, 0);
56     glBindVertexArray(0);
57 
58     // glDeleteBuffers (1, &buffer);
59     vbo_out = buffer;
60     ebo_out = ebuffer;
61 
62     return vao;
63 }
64 
TriangleRenderer(const CanvasGL & c,const std::map<int,vector_pair<Triangle,TriangleInfo>> & tris)65 TriangleRenderer::TriangleRenderer(const CanvasGL &c, const std::map<int, vector_pair<Triangle, TriangleInfo>> &tris)
66     : ca(c), triangles(tris)
67 {
68 }
69 
70 
realize()71 void TriangleRenderer::realize()
72 {
73 
74     glGenTextures(1, &texture_glyph);
75     glActiveTexture(GL_TEXTURE0);
76     glBindTexture(GL_TEXTURE_2D, texture_glyph);
77     bitmap_font::load_texture();
78     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
79     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
80 
81     program_triangle = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
82                                                        "/org/horizon-eda/horizon/canvas/shaders/"
83                                                        "triangle-triangle-fragment.glsl",
84                                                        "/org/horizon-eda/horizon/canvas/shaders/"
85                                                        "triangle-triangle-geometry.glsl");
86     program_line0 = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
87                                                     "/org/horizon-eda/horizon/canvas/shaders/"
88                                                     "triangle-line0-fragment.glsl",
89                                                     "/org/horizon-eda/horizon/canvas/shaders/"
90                                                     "triangle-line0-geometry.glsl");
91     program_line_butt = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
92                                                         "/org/horizon-eda/horizon/canvas/shaders/"
93                                                         "triangle-line-butt-fragment.glsl",
94                                                         "/org/horizon-eda/horizon/canvas/shaders/"
95                                                         "triangle-line-butt-geometry.glsl");
96     program_line = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
97                                                    "/org/horizon-eda/horizon/canvas/shaders/"
98                                                    "triangle-line-fragment.glsl",
99                                                    "/org/horizon-eda/horizon/canvas/shaders/"
100                                                    "triangle-line-geometry.glsl");
101     program_glyph = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
102                                                     "/org/horizon-eda/horizon/canvas/shaders/"
103                                                     "triangle-glyph-fragment.glsl",
104                                                     "/org/horizon-eda/horizon/canvas/shaders/"
105                                                     "triangle-glyph-geometry.glsl");
106     program_circle = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
107                                                      "/org/horizon-eda/horizon/canvas/shaders/"
108                                                      "triangle-circle-fragment.glsl",
109                                                      "/org/horizon-eda/horizon/canvas/shaders/"
110                                                      "triangle-circle-geometry.glsl");
111     program_arc0 = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
112                                                    "/org/horizon-eda/horizon/canvas/shaders/"
113                                                    "triangle-arc0-fragment.glsl",
114                                                    "/org/horizon-eda/horizon/canvas/shaders/"
115                                                    "triangle-arc0-geometry.glsl");
116     program_arc = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/triangle-vertex.glsl",
117                                                   "/org/horizon-eda/horizon/canvas/shaders/"
118                                                   "triangle-arc-fragment.glsl",
119                                                   "/org/horizon-eda/horizon/canvas/shaders/"
120                                                   "triangle-arc-geometry.glsl");
121     GL_CHECK_ERROR;
122     glGenBuffers(1, &ubo);
123     glBindBuffer(GL_UNIFORM_BUFFER, ubo);
124     float testd[3] = {1, 1, 1};
125     glBufferData(GL_UNIFORM_BUFFER, sizeof(testd), &testd, GL_DYNAMIC_DRAW);
126     glBindBuffer(GL_UNIFORM_BUFFER, 0);
127     GL_CHECK_ERROR;
128     unsigned int block_index = glGetUniformBlockIndex(program_line, "layer_setup");
129     GLuint binding_point_index = 0;
130     glBindBufferBase(GL_UNIFORM_BUFFER, binding_point_index, ubo);
131     glUniformBlockBinding(program_triangle, block_index, binding_point_index);
132     glUniformBlockBinding(program_line0, block_index, binding_point_index);
133     glUniformBlockBinding(program_line_butt, block_index, binding_point_index);
134     glUniformBlockBinding(program_line, block_index, binding_point_index);
135     glUniformBlockBinding(program_glyph, block_index, binding_point_index);
136     glUniformBlockBinding(program_circle, block_index, binding_point_index);
137     glUniformBlockBinding(program_arc, block_index, binding_point_index);
138     glUniformBlockBinding(program_arc0, block_index, binding_point_index);
139     GL_CHECK_ERROR;
140     vao = create_vao(program_line0, vbo, ebo);
141     GL_CHECK_ERROR;
142 }
143 
144 
145 class UBOBuffer {
146 public:
147     static constexpr size_t ubo_size = 20; //  keep in sync with ubo.glsl
148     static_assert(static_cast<int>(ColorP::N_COLORS) == ubo_size, "ubo size mismatch");
149     std::array<std::array<float, 4>, ubo_size> colors;
150     std::array<std::array<float, 4>, 256> colors2; // keep in sync with shader
151     std::array<float, 12> screenmat;
152     std::array<float, 12> viewmat;
153     float alpha;
154     float scale;
155     std::array<float, 2> offset;
156     float min_line_width;
157     unsigned int layer_mode;
158     unsigned int stencil_mode;
159 };
160 
operator +(const std::array<float,4> & a,float b)161 static std::array<float, 4> operator+(const std::array<float, 4> &a, float b)
162 {
163     return {a.at(0) + b, a.at(1) + b, a.at(2) + b, a.at(3)};
164 }
165 
operator +(const std::array<float,4> & a,const std::array<float,4> & b)166 static std::array<float, 4> operator+(const std::array<float, 4> &a, const std::array<float, 4> &b)
167 {
168     return {a.at(0) + b.at(0), a.at(1) + b.at(1), a.at(2) + b.at(2), a.at(3)};
169 }
170 
operator *(const std::array<float,4> & a,float b)171 static std::array<float, 4> operator*(const std::array<float, 4> &a, float b)
172 {
173     return {a.at(0) * b, a.at(1) * b, a.at(2) * b, a.at(3)};
174 }
175 
apply_highlight(const Color & icolor,HighlightMode mode,int layer) const176 std::array<float, 4> TriangleRenderer::apply_highlight(const Color &icolor, HighlightMode mode, int layer) const
177 {
178     const std::array<float, 4> color = gl_array_from_color(icolor);
179     if (layer >= 20000 && layer < 30000) { // is annotation
180         if (!ca.annotations.at(layer).use_highlight)
181             return color;
182     }
183     if (ca.layer_mode == CanvasGL::LayerMode::SHADOW_OTHER) {
184         if (layer == ca.work_layer || ca.is_overlay_layer(layer, ca.work_layer)) {
185             // it's okay continue as usual
186         }
187         else {
188             if (mode == HighlightMode::ONLY)
189                 return color;
190             else
191                 return gl_array_from_color(ca.appearance.colors.at(ColorP::SHADOW));
192         }
193     }
194     if (!ca.highlight_enabled)
195         return color;
196     switch (ca.highlight_mode) {
197     case CanvasGL::HighlightMode::HIGHLIGHT:
198         if (mode == HighlightMode::ONLY)
199             return color + ca.appearance.highlight_lighten;
200         else
201             return color;
202 
203     case CanvasGL::HighlightMode::DIM:
204         if (mode == HighlightMode::ONLY)
205             return color;
206         else
207             return (color * ca.appearance.highlight_dim)
208                    + (gl_array_from_color(ca.appearance.colors.at(ColorP::BACKGROUND))
209                       * (1 - ca.appearance.highlight_dim));
210 
211     case CanvasGL::HighlightMode::SHADOW:
212         if (mode == HighlightMode::ONLY)
213             return color;
214         else
215             return gl_array_from_color(ca.appearance.colors.at(ColorP::SHADOW));
216     }
217     return color;
218 }
219 
render_layer_batch(int layer,HighlightMode highlight_mode,bool ignore_flip,const Batch & batch,bool use_stencil,bool stencil_mode)220 void TriangleRenderer::render_layer_batch(int layer, HighlightMode highlight_mode, bool ignore_flip, const Batch &batch,
221                                           bool use_stencil, bool stencil_mode)
222 {
223     const auto &ld = ca.get_layer_display(layer);
224     UBOBuffer buf;
225 
226     buf.alpha = ca.property_layer_opacity() / 100;
227     gl_mat3_to_array(buf.screenmat, ca.screenmat);
228     if (ignore_flip)
229         gl_mat3_to_array(buf.viewmat, ca.viewmat_noflip);
230     else
231         gl_mat3_to_array(buf.viewmat, ca.viewmat);
232 
233     buf.layer_mode = static_cast<unsigned int>(ld.mode);
234     buf.scale = ca.scale;
235     buf.offset[0] = ca.offset.x;
236     buf.offset[1] = ca.offset.y;
237     buf.min_line_width = ca.appearance.min_line_width;
238     buf.stencil_mode = stencil_mode;
239 
240     for (const auto &[key, span] : batch) {
241         bool skip = false;
242         switch (key.type) {
243         case Type::TRIANGLE:
244             glUseProgram(program_triangle);
245             if (ld.mode == LayerDisplay::Mode::OUTLINE)
246                 skip = true;
247             break;
248 
249         case Type::LINE0:
250             glUseProgram(program_line0);
251             break;
252 
253         case Type::LINE_BUTT:
254             glUseProgram(program_line_butt);
255             break;
256 
257         case Type::LINE:
258             glUseProgram(program_line);
259             break;
260 
261         case Type::GLYPH:
262             glUseProgram(program_glyph);
263             break;
264 
265         case Type::CIRCLE:
266             glUseProgram(program_circle);
267             break;
268 
269         case Type::ARC0:
270             glUseProgram(program_arc0);
271             break;
272 
273         case Type::ARC:
274             glUseProgram(program_arc);
275             break;
276         }
277         switch (highlight_mode) {
278         case HighlightMode::ONLY: // only highlighted, skip not highlighted
279             if (!key.highlight) {
280                 skip = true;
281             }
282             break;
283         case HighlightMode::SKIP: // only not highlighted, skip highlighted
284             if (key.highlight) {
285                 skip = true;
286             }
287             break;
288         }
289         if (!skip) {
290             for (size_t i = 0; i < buf.colors.size(); i++) {
291                 auto k = static_cast<ColorP>(i);
292                 if (ca.appearance.colors.count(k))
293                     buf.colors[i] = apply_highlight(ca.appearance.colors.at(k), highlight_mode, layer);
294             }
295             auto lc = ca.get_layer_color(layer);
296             buf.colors[static_cast<int>(ColorP::AIRWIRE_ROUTER)] =
297                     gl_array_from_color(ca.appearance.colors.at(ColorP::AIRWIRE_ROUTER));
298             buf.colors[static_cast<int>(ColorP::FROM_LAYER)] = apply_highlight(lc, highlight_mode, layer);
299             buf.colors[static_cast<int>(ColorP::LAYER_HIGHLIGHT)] =
300                     gl_array_from_color(lc) + ca.appearance.highlight_lighten;
301             for (size_t i = 0; i < std::min(buf.colors2.size(), ca.colors2.size()); i++) {
302                 buf.colors2[i] = apply_highlight(ca.colors2[i].to_color(), highlight_mode, layer);
303             }
304 
305             if (ld.mode == LayerDisplay::Mode::FILL_ONLY || (key.stencil && use_stencil))
306                 glStencilFunc(GL_GREATER, stencil, 0xff);
307             else
308                 glStencilFunc(GL_ALWAYS, stencil, 0xff);
309 
310 
311             glBindBuffer(GL_UNIFORM_BUFFER, ubo);
312             glBufferData(GL_UNIFORM_BUFFER, sizeof(buf), &buf, GL_DYNAMIC_DRAW);
313             glBindBuffer(GL_UNIFORM_BUFFER, 0);
314             GL_CHECK_ERROR
315             glDrawElements(GL_POINTS, span.count, GL_UNSIGNED_INT, (void *)(span.offset * sizeof(unsigned int)));
316         }
317     }
318 }
319 
render_layer(int layer,HighlightMode highlight_mode,bool ignore_flip)320 void TriangleRenderer::render_layer(int layer, HighlightMode highlight_mode, bool ignore_flip)
321 {
322     GL_CHECK_ERROR
323     if (layer_offsets.count(layer)) {
324         const auto &ld = ca.get_layer_display(layer);
325 
326         const auto &batches = layer_offsets.at(layer);
327         Batch batch_stencil, batch_no_stencil;
328         for (const auto &[key, span] : batches) {
329             if (key.stencil)
330                 batch_stencil.emplace_back(key, span);
331             else
332                 batch_no_stencil.emplace_back(key, span);
333         }
334         if (ld.mode == LayerDisplay::Mode::FILL_ONLY) {
335             render_layer_batch(layer, highlight_mode, ignore_flip, batch_stencil, false, false);
336             render_layer_batch(layer, highlight_mode, ignore_flip, batch_no_stencil, false, false);
337         }
338         else {
339             // draw stencil first
340             render_layer_batch(layer, highlight_mode, ignore_flip, batch_stencil, true, true);
341             render_layer_batch(layer, highlight_mode, ignore_flip, batch_stencil, true, false);
342 
343             render_layer_batch(layer, highlight_mode, ignore_flip, batch_no_stencil, false, false);
344         }
345     }
346     // glDrawArrays(GL_POINTS, layer_offsets[layer], triangles[layer].size());
347     stencil++;
348 }
349 
render()350 void TriangleRenderer::render()
351 {
352     glBindVertexArray(vao);
353     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
354     glActiveTexture(GL_TEXTURE0);
355 
356     GL_CHECK_ERROR
357 
358     std::vector<int> layers;
359     layers.reserve(layer_offsets.size());
360     for (const auto &it : layer_offsets) {
361         if (ca.get_layer_display(it.first).visible)
362             layers.push_back(it.first);
363     }
364     std::sort(layers.begin(), layers.end());
365     if (ca.work_layer < 0) {
366         std::reverse(layers.begin(), layers.end());
367     }
368 
369     glClear(GL_STENCIL_BUFFER_BIT);
370     glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
371     glEnable(GL_STENCIL_TEST);
372     stencil = 1;
373 
374     static const std::vector<std::vector<HighlightMode>> modes_on_top = {{HighlightMode::SKIP}, {HighlightMode::ONLY}};
375     static const std::vector<std::vector<HighlightMode>> modes_normal = {{HighlightMode::SKIP, HighlightMode::ONLY}};
376 
377     const auto &modes = ca.highlight_on_top ? modes_on_top : modes_normal;
378 
379     std::vector<std::pair<int, std::set<std::pair<int, bool>>>> normal_layers;
380     normal_layers.reserve(layers.size());
381     for (const auto layer : layers) {
382         const auto &ld = ca.get_layer_display(layer);
383         if (layer != ca.work_layer && layer < 10000 && ld.visible && !ca.layer_is_annotation(layer))
384             normal_layers.push_back({layer, {}});
385     }
386     normal_layers.push_back({ca.work_layer, {}});
387 
388     for (const auto &[k, overlay_layer] : ca.overlay_layers) {
389         const auto layer = k.first;
390         const auto ignore_flip = k.second;
391         auto f = std::find_if(normal_layers.rbegin(), normal_layers.rend(),
392                               [layer](const auto &x) { return layer.overlaps(x.first); });
393         if (f != normal_layers.rend()) {
394             f->second.emplace(overlay_layer, ignore_flip);
395         }
396     }
397 
398     render_annotations(false); // annotation bottom
399     for (const auto &highlight_modes : modes) {
400         for (const auto &[layer, overlays] : normal_layers) {
401             for (const auto highlight_mode : highlight_modes) {
402                 if (layer != ca.work_layer && ca.layer_mode == CanvasGL::LayerMode::WORK_ONLY
403                     && highlight_mode == HighlightMode::SKIP)
404                     continue;
405 
406                 render_layer(layer, highlight_mode);
407                 for (const auto &[overlay, ignore_flip] : overlays) {
408                     render_layer(overlay, highlight_mode, ignore_flip);
409                 }
410             }
411         }
412         for (auto layer : layers) {
413             const auto &ld = ca.get_layer_display(layer);
414             if (layer >= 10000 && layer < Canvas::first_overlay_layer && ld.visible && !ca.layer_is_annotation(layer)) {
415                 for (const auto highlight_mode : highlight_modes) {
416                     render_layer(layer, highlight_mode);
417                 }
418             }
419         }
420     }
421     render_annotations(true); // anotations top
422     glDisable(GL_STENCIL_TEST);
423 
424     GL_CHECK_ERROR
425 
426     glBindVertexArray(0);
427     glUseProgram(0);
428     GL_CHECK_ERROR
429 }
430 
render_annotations(bool top)431 void TriangleRenderer::render_annotations(bool top)
432 {
433     for (const auto &it : ca.annotations) {
434         if (ca.get_layer_display(it.first).visible && it.second.on_top == top) {
435             render_layer(it.first, HighlightMode::SKIP);
436             render_layer(it.first, HighlightMode::ONLY);
437         }
438     }
439 }
440 
push()441 void TriangleRenderer::push()
442 {
443     GL_CHECK_ERROR
444     glBindBuffer(GL_ARRAY_BUFFER, vbo);
445     n_tris = 0;
446     for (const auto &it : triangles) {
447         n_tris += it.second.size();
448     }
449     glBufferData(GL_ARRAY_BUFFER, sizeof(Triangle) * n_tris, nullptr, GL_STREAM_DRAW);
450     GL_CHECK_ERROR
451     size_t ofs = 0;
452     layer_offsets.clear();
453     std::vector<unsigned int> elements;
454     for (const auto &[layer, tris] : triangles) {
455         const auto &ld = ca.get_layer_display(layer);
456         glBufferSubData(GL_ARRAY_BUFFER, ofs * sizeof(Triangle), tris.size() * sizeof(Triangle), tris.first.data());
457         std::map<BatchKey, std::vector<unsigned int>> type_indices;
458         unsigned int i = 0;
459         for (const auto &[tri, tri_info] : tris) {
460             const bool hidden = tri_info.flags & TriangleInfo::FLAG_HIDDEN;
461             const bool type_visible = ld.types_visible & (1 << static_cast<int>(tri_info.type));
462             if (!hidden && type_visible) {
463                 auto ty = Type::LINE;
464                 if (tri_info.flags & TriangleInfo::FLAG_GLYPH) {
465                     ty = Type::GLYPH;
466                 }
467                 else if ((tri_info.flags & TriangleInfo::FLAG_ARC) && tri.y2 == 0) {
468                     ty = Type::ARC0;
469                 }
470                 else if (tri_info.flags & TriangleInfo::FLAG_ARC) {
471                     ty = Type::ARC;
472                 }
473                 else if (tri_info.flags & TriangleInfo::FLAG_BUTT) {
474                     ty = Type::LINE_BUTT;
475                 }
476                 else if (!std::isnan(tri.y2)) {
477                     ty = Type::TRIANGLE;
478                 }
479                 else if (std::isnan(tri.y2) && tri.x2 == 0) {
480                     ty = Type::LINE0;
481                 }
482                 else if (std::isnan(tri.y1) && std::isnan(tri.x2) && std::isnan(tri.y2)) {
483                     ty = Type::CIRCLE;
484                 }
485 
486                 else if (std::isnan(tri.y2)) {
487                     ty = Type::LINE;
488                 }
489                 else {
490                     throw std::runtime_error("unknown triangle type");
491                 }
492                 const bool highlight = (tri_info.flags & TriangleInfo::FLAG_HIGHLIGHT)
493                                        || (tri.color == static_cast<int>(ColorP::LAYER_HIGHLIGHT));
494                 const bool do_stencil = tri_info.type == TriangleInfo::Type::PAD;
495                 const BatchKey key{ty, highlight, do_stencil};
496                 type_indices[key].push_back(i + ofs);
497             }
498             i++;
499         }
500         for (const auto &[key, elems] : type_indices) {
501             auto el_offset = elements.size();
502             elements.insert(elements.end(), elems.begin(), elems.end());
503             layer_offsets[layer][key] = {el_offset, elems.size()};
504         }
505         ofs += tris.size();
506     }
507     glBindBuffer(GL_ARRAY_BUFFER, 0);
508     GL_CHECK_ERROR
509     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
510     glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * elements.size(), elements.data(), GL_STATIC_DRAW);
511     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
512 
513     GL_CHECK_ERROR
514 }
515 } // namespace horizon
516