Line data Source code
1 :
2 : /* Copyright (c) 2008-2013, Stefan Eilemann <eile@equalizergraphics.com>
3 : * 2010, Cedric Stalder <cedric.stalder@gmail.com>
4 : *
5 : * This library is free software; you can redistribute it and/or modify it under
6 : * the terms of the GNU Lesser General Public License version 2.1 as published
7 : * by the Free Software Foundation.
8 : *
9 : * This library is distributed in the hope that it will be useful, but WITHOUT
10 : * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 : * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 : * details.
13 : *
14 : * You should have received a copy of the GNU Lesser General Public License
15 : * along with this library; if not, write to the Free Software Foundation, Inc.,
16 : * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 : */
18 :
19 : #include "treeEqualizer.h"
20 :
21 : #include "../compound.h"
22 : #include "../log.h"
23 :
24 : #include <eq/fabric/statistic.h>
25 : #include <lunchbox/debug.h>
26 :
27 : namespace eq
28 : {
29 : namespace server
30 : {
31 : std::ostream& operator<<(std::ostream& os, const TreeEqualizer::Node*);
32 :
33 : // The tree load balancer organizes the children in a binary tree. At each
34 : // level, a relative split position is determined by balancing the left subtree
35 : // against the right subtree.
36 :
37 0 : TreeEqualizer::TreeEqualizer()
38 0 : : _tree(0)
39 : {
40 0 : LBINFO << "New TreeEqualizer @" << (void*)this << std::endl;
41 0 : }
42 :
43 0 : TreeEqualizer::TreeEqualizer(const TreeEqualizer& from)
44 : : Equalizer(from)
45 : , ChannelListener(from)
46 0 : , _tree(0)
47 : {
48 0 : }
49 :
50 0 : TreeEqualizer::~TreeEqualizer()
51 : {
52 0 : _clearTree(_tree);
53 0 : delete _tree;
54 0 : _tree = 0;
55 0 : }
56 :
57 0 : void TreeEqualizer::notifyUpdatePre(Compound* compound,
58 : const uint32_t /*frame*/)
59 : {
60 0 : if (isFrozen() || !compound->isActive() || !isActive())
61 0 : return;
62 :
63 0 : if (!_tree)
64 : {
65 0 : LBASSERT(compound == getCompound());
66 0 : const Compounds& children = compound->getChildren();
67 0 : switch (children.size())
68 : {
69 : case 0:
70 0 : return; // no leaf compound, can't do anything.
71 : case 1: // one child, 'balance' it:
72 0 : if (getMode() == MODE_DB)
73 0 : children.front()->setRange(Range());
74 : else
75 0 : children.front()->setViewport(Viewport());
76 0 : return;
77 : default:
78 0 : _tree = _buildTree(children);
79 : }
80 : }
81 :
82 : // compute new data
83 0 : _update(_tree);
84 0 : _split(_tree);
85 0 : _assign(_tree, Viewport(), Range());
86 0 : LBLOG(LOG_LB2) << "LB tree: " << _tree;
87 : }
88 :
89 0 : TreeEqualizer::Node* TreeEqualizer::_buildTree(const Compounds& compounds)
90 : {
91 0 : Node* node = new Node;
92 :
93 0 : const size_t size = compounds.size();
94 0 : if (size == 1)
95 : {
96 0 : Compound* compound = compounds.front();
97 :
98 0 : node->compound = compound;
99 :
100 0 : Channel* channel = compound->getChannel();
101 0 : LBASSERT(channel);
102 0 : channel->addListener(this);
103 0 : return node;
104 : }
105 :
106 0 : const size_t middle = size >> 1;
107 :
108 0 : Compounds left;
109 0 : for (size_t i = 0; i < middle; ++i)
110 0 : left.push_back(compounds[i]);
111 :
112 0 : Compounds right;
113 0 : for (size_t i = middle; i < size; ++i)
114 0 : right.push_back(compounds[i]);
115 :
116 0 : node->left = _buildTree(left);
117 0 : node->right = _buildTree(right);
118 0 : return node;
119 : }
120 :
121 0 : void TreeEqualizer::_clearTree(Node* node)
122 : {
123 0 : if (!node)
124 0 : return;
125 :
126 0 : if (node->compound)
127 : {
128 0 : Channel* channel = node->compound->getChannel();
129 0 : LBASSERTINFO(channel, node->compound);
130 0 : channel->removeListener(this);
131 : }
132 : else
133 : {
134 0 : _clearTree(node->left);
135 0 : _clearTree(node->right);
136 : }
137 : }
138 :
139 0 : void TreeEqualizer::notifyLoadData(Channel* channel, const uint32_t /*frame*/,
140 : const Statistics& statistics,
141 : const Viewport& /*region*/)
142 : {
143 0 : _notifyLoadData(_tree, channel, statistics);
144 0 : }
145 :
146 0 : void TreeEqualizer::_notifyLoadData(Node* node, Channel* channel,
147 : const Statistics& statistics)
148 : {
149 0 : if (!node)
150 0 : return;
151 :
152 0 : _notifyLoadData(node->left, channel, statistics);
153 0 : _notifyLoadData(node->right, channel, statistics);
154 :
155 0 : if (!node->compound || node->compound->getChannel() != channel)
156 0 : return;
157 :
158 : // gather relevant load data
159 0 : const uint32_t taskID = node->compound->getTaskID();
160 0 : int64_t startTime = std::numeric_limits<int64_t>::max();
161 0 : int64_t endTime = 0;
162 0 : bool loadSet = false;
163 0 : int64_t timeTransmit = 0;
164 0 : for (size_t i = 0; i < statistics.size() && !loadSet; ++i)
165 : {
166 0 : const Statistic& stat = statistics[i];
167 0 : if (stat.task != taskID) // from different compound
168 0 : continue;
169 :
170 0 : switch (stat.type)
171 : {
172 : case Statistic::CHANNEL_CLEAR:
173 : case Statistic::CHANNEL_DRAW:
174 : case Statistic::CHANNEL_READBACK:
175 0 : startTime = LB_MIN(startTime, stat.startTime);
176 0 : endTime = LB_MAX(endTime, stat.endTime);
177 0 : break;
178 :
179 : case Statistic::CHANNEL_ASYNC_READBACK:
180 : case Statistic::CHANNEL_FRAME_TRANSMIT:
181 0 : timeTransmit += stat.endTime - stat.startTime;
182 0 : break;
183 :
184 : // assemble blocks on input frames, stop using subsequent data
185 : case Statistic::CHANNEL_ASSEMBLE:
186 0 : loadSet = true;
187 0 : break;
188 :
189 : default:
190 0 : break;
191 : }
192 : }
193 :
194 0 : if (startTime == std::numeric_limits<int64_t>::max())
195 0 : return;
196 :
197 0 : node->time = endTime - startTime;
198 0 : node->time = LB_MAX(node->time, 1);
199 0 : node->time = LB_MAX(node->time, timeTransmit);
200 : }
201 :
202 0 : void TreeEqualizer::_update(Node* node)
203 : {
204 0 : if (!node)
205 0 : return;
206 :
207 0 : const Compound* compound = node->compound;
208 0 : if (compound)
209 : {
210 0 : const Channel* channel = compound->getChannel();
211 0 : const PixelViewport& pvp = channel->getPixelViewport();
212 0 : LBASSERT(channel);
213 :
214 0 : LBASSERT(node->mode != MODE_2D);
215 0 : node->resources = compound->isActive() ? compound->getUsage() : 0.f;
216 0 : node->maxSize.x() = pvp.w;
217 0 : node->maxSize.y() = pvp.h;
218 0 : node->boundaryf = getBoundaryf();
219 0 : node->boundary2i = getBoundary2i();
220 0 : node->resistancef = getResistancef();
221 0 : node->resistance2i = getResistance2i();
222 0 : return;
223 : }
224 : // else
225 :
226 0 : LBASSERT(node->left);
227 0 : LBASSERT(node->right);
228 :
229 0 : node->left->mode = node->right->mode = node->mode = getMode();
230 :
231 0 : if (node->mode == MODE_2D)
232 0 : node->mode = MODE_VERTICAL;
233 :
234 0 : if (node->left->mode == MODE_2D)
235 : {
236 0 : LBASSERT(node->right->mode == MODE_2D);
237 :
238 0 : node->left->mode =
239 0 : (node->mode == MODE_VERTICAL) ? MODE_HORIZONTAL : MODE_VERTICAL;
240 0 : node->right->mode = node->left->mode;
241 : }
242 :
243 0 : _update(node->left);
244 0 : _update(node->right);
245 :
246 0 : node->resources = node->left->resources + node->right->resources;
247 :
248 0 : if (node->left->resources == 0.f)
249 : {
250 0 : node->maxSize = node->right->maxSize;
251 0 : node->boundary2i = node->right->boundary2i;
252 0 : node->boundaryf = node->right->boundaryf;
253 0 : node->resistance2i = node->right->resistance2i;
254 0 : node->resistancef = node->right->resistancef;
255 0 : node->time = node->right->time;
256 : }
257 0 : else if (node->right->resources == 0.f)
258 : {
259 0 : node->maxSize = node->left->maxSize;
260 0 : node->boundary2i = node->left->boundary2i;
261 0 : node->boundaryf = node->left->boundaryf;
262 0 : node->resistance2i = node->left->resistance2i;
263 0 : node->resistancef = node->left->resistancef;
264 0 : node->time = node->left->time;
265 : }
266 : else
267 : {
268 0 : switch (node->mode)
269 : {
270 : case MODE_VERTICAL:
271 0 : node->maxSize.x() =
272 0 : node->left->maxSize.x() + node->right->maxSize.x();
273 0 : node->maxSize.y() =
274 0 : LB_MIN(node->left->maxSize.y(), node->right->maxSize.y());
275 0 : node->boundary2i.x() =
276 0 : node->left->boundary2i.x() + node->right->boundary2i.x();
277 0 : node->boundary2i.y() =
278 0 : LB_MAX(node->left->boundary2i.y(), node->right->boundary2i.y());
279 0 : node->boundaryf =
280 0 : LB_MAX(node->left->boundaryf, node->right->boundaryf);
281 0 : node->resistance2i.x() = LB_MAX(node->left->resistance2i.x(),
282 : node->right->resistance2i.x());
283 0 : node->resistance2i.y() = LB_MAX(node->left->resistance2i.y(),
284 : node->right->resistance2i.y());
285 0 : node->resistancef =
286 0 : LB_MAX(node->left->resistancef, node->right->resistancef);
287 0 : break;
288 : case MODE_HORIZONTAL:
289 0 : node->maxSize.x() =
290 0 : LB_MIN(node->left->maxSize.x(), node->right->maxSize.x());
291 0 : node->maxSize.y() =
292 0 : node->left->maxSize.y() + node->right->maxSize.y();
293 0 : node->boundary2i.x() =
294 0 : LB_MAX(node->left->boundary2i.x(), node->right->boundary2i.x());
295 0 : node->boundary2i.y() =
296 0 : node->left->boundary2i.y() + node->right->boundary2i.y();
297 0 : node->boundaryf =
298 0 : LB_MAX(node->left->boundaryf, node->right->boundaryf);
299 0 : node->resistance2i.x() = LB_MAX(node->left->resistance2i.x(),
300 : node->right->resistance2i.x());
301 0 : node->resistance2i.y() = LB_MAX(node->left->resistance2i.y(),
302 : node->right->resistance2i.y());
303 0 : node->resistancef =
304 0 : LB_MAX(node->left->resistancef, node->right->resistancef);
305 0 : break;
306 : case MODE_DB:
307 0 : node->boundary2i.x() =
308 0 : LB_MAX(node->left->boundary2i.x(), node->right->boundary2i.x());
309 0 : node->boundary2i.y() =
310 0 : LB_MAX(node->left->boundary2i.y(), node->right->boundary2i.y());
311 0 : node->boundaryf = node->left->boundaryf + node->right->boundaryf;
312 0 : node->resistance2i.x() = LB_MAX(node->left->resistance2i.x(),
313 : node->right->resistance2i.x());
314 0 : node->resistance2i.y() = LB_MAX(node->left->resistance2i.y(),
315 : node->right->resistance2i.y());
316 0 : node->resistancef =
317 0 : LB_MAX(node->left->resistancef, node->right->resistancef);
318 0 : break;
319 : default:
320 0 : LBUNIMPLEMENTED;
321 : }
322 :
323 0 : node->time = node->left->time + node->right->time;
324 : }
325 : }
326 :
327 0 : void TreeEqualizer::_split(Node* node)
328 : {
329 0 : if (node->compound)
330 0 : return;
331 0 : LBASSERT(node->left && node->right);
332 :
333 0 : Node* left = node->left;
334 0 : Node* right = node->right;
335 : // easy outs
336 0 : if (left->resources == 0.f)
337 : {
338 0 : node->split = 0.f;
339 0 : return;
340 : }
341 0 : if (right->resources == 0.f)
342 : {
343 0 : node->split = 1.f;
344 0 : return;
345 : }
346 :
347 : // new split
348 0 : const float target = node->time * left->resources / node->resources;
349 0 : const float leftTime = float(left->time);
350 0 : float split = 0.f;
351 0 : const float rightTime = float(right->time);
352 :
353 0 : if (leftTime >= target)
354 0 : split = target / leftTime * node->split;
355 : else
356 : {
357 0 : const float timeLeft = target - leftTime;
358 0 : split = node->split + timeLeft / rightTime * (1.f - node->split);
359 : }
360 :
361 0 : LBLOG(LOG_LB2) << "Should split at " << split << " (" << target << ": "
362 0 : << leftTime << " by " << left->resources << "/" << rightTime
363 0 : << " by " << right->resources << ")" << std::endl;
364 0 : node->split = (1.f - getDamping()) * split + getDamping() * node->split;
365 0 : LBLOG(LOG_LB2) << "Dampened split at " << node->split << std::endl;
366 :
367 0 : _split(left);
368 0 : _split(right);
369 : }
370 :
371 0 : void TreeEqualizer::_assign(Node* node, const Viewport& vp, const Range& range)
372 : {
373 0 : LBLOG(LOG_LB2) << "assign " << vp << ", " << range << " time " << node->time
374 0 : << " split " << node->split << std::endl;
375 0 : LBASSERTINFO(vp.isValid(), vp);
376 0 : LBASSERTINFO(range.isValid(), range);
377 0 : LBASSERTINFO(node->resources > 0.f || !vp.hasArea() || !range.hasData(),
378 : "Assigning work to unused compound: " << vp << ", " << range);
379 :
380 0 : Compound* compound = node->compound;
381 0 : if (compound)
382 : {
383 0 : LBASSERTINFO(vp == Viewport::FULL || range == Range::ALL,
384 : "Mixed 2D/DB load-balancing not implemented");
385 :
386 0 : compound->setViewport(vp);
387 0 : compound->setRange(range);
388 0 : LBLOG(LOG_LB2) << compound->getChannel()->getName() << " set " << vp
389 0 : << ", " << range << std::endl;
390 0 : return;
391 : }
392 :
393 0 : switch (node->mode)
394 : {
395 : case MODE_VERTICAL:
396 : {
397 : // Ensure minimum size
398 0 : const Compound* root = getCompound();
399 0 : const float pvpW = float(root->getInheritPixelViewport().w);
400 0 : const float end = vp.getXEnd();
401 0 : const float boundary = float(node->boundary2i.x()) / pvpW;
402 0 : float absoluteSplit = vp.x + vp.w * node->split;
403 :
404 0 : if (node->left->resources == 0.f)
405 0 : absoluteSplit = vp.x;
406 0 : else if (node->right->resources == 0.f)
407 0 : absoluteSplit = end;
408 0 : else if (boundary > 0)
409 : {
410 0 : const float right = vp.getXEnd() - absoluteSplit;
411 0 : const float left = absoluteSplit - vp.x;
412 0 : const float maxRight = float(node->right->maxSize.x()) / pvpW;
413 0 : const float maxLeft = float(node->left->maxSize.x()) / pvpW;
414 :
415 0 : if (right > maxRight)
416 0 : absoluteSplit = end - maxRight;
417 0 : else if (left > maxLeft)
418 0 : absoluteSplit = vp.x + maxLeft;
419 :
420 0 : if ((absoluteSplit - vp.x) < boundary)
421 0 : absoluteSplit = vp.x + boundary;
422 0 : if ((end - absoluteSplit) < boundary)
423 0 : absoluteSplit = end - boundary;
424 :
425 0 : const uint32_t ratio = uint32_t(absoluteSplit / boundary + .5f);
426 0 : absoluteSplit = ratio * boundary;
427 : }
428 :
429 0 : absoluteSplit = LB_MAX(absoluteSplit, vp.x);
430 0 : absoluteSplit = LB_MIN(absoluteSplit, end);
431 :
432 0 : const float newPixelW = pvpW * node->split;
433 0 : const float oldPixelW = pvpW * node->oldsplit;
434 0 : if (int(fabs(newPixelW - oldPixelW)) < node->resistance2i.x())
435 : {
436 0 : absoluteSplit = vp.x + vp.w * node->oldsplit;
437 0 : node->split = node->oldsplit;
438 : }
439 : else
440 : {
441 0 : node->split = (absoluteSplit - vp.x) / vp.w;
442 0 : node->oldsplit = node->split;
443 : }
444 :
445 0 : LBLOG(LOG_LB2) << "Constrained split " << vp << " at X " << node->split
446 0 : << std::endl;
447 :
448 : // traverse children
449 0 : Viewport childVP = vp;
450 0 : childVP.w = (absoluteSplit - vp.x);
451 0 : _assign(node->left, childVP, range);
452 :
453 0 : childVP.x = childVP.getXEnd();
454 0 : childVP.w = end - childVP.x;
455 :
456 : // Fix 2994111: Rounding errors with 2D LB and 16 sources
457 : // Floating point rounding may create a width for the 'right'
458 : // child which is slightly below the parent width. Correct it.
459 0 : while (childVP.getXEnd() < end)
460 0 : childVP.w += std::numeric_limits<float>::epsilon();
461 :
462 0 : _assign(node->right, childVP, range);
463 0 : break;
464 : }
465 :
466 : case MODE_HORIZONTAL:
467 : {
468 : // Ensure minimum size
469 0 : const Compound* root = getCompound();
470 0 : const float pvpH = float(root->getInheritPixelViewport().h);
471 0 : const float end = vp.getYEnd();
472 0 : const float boundary = float(node->boundary2i.y()) / pvpH;
473 0 : float absoluteSplit = vp.y + vp.h * node->split;
474 :
475 0 : if (node->left->resources == 0.f)
476 0 : absoluteSplit = vp.y;
477 0 : else if (node->right->resources == 0.f)
478 0 : absoluteSplit = end;
479 0 : else if (boundary > 0)
480 : {
481 0 : const float right = vp.getYEnd() - absoluteSplit;
482 0 : const float left = absoluteSplit - vp.y;
483 0 : const float maxRight = float(node->right->maxSize.y()) / pvpH;
484 0 : const float maxLeft = float(node->left->maxSize.y()) / pvpH;
485 :
486 0 : if (right > maxRight)
487 0 : absoluteSplit = end - maxRight;
488 0 : else if (left > maxLeft)
489 0 : absoluteSplit = vp.y + maxLeft;
490 :
491 0 : if ((absoluteSplit - vp.y) < boundary)
492 0 : absoluteSplit = vp.y + boundary;
493 0 : if ((end - absoluteSplit) < boundary)
494 0 : absoluteSplit = end - boundary;
495 :
496 0 : const uint32_t ratio = uint32_t(absoluteSplit / boundary + .5f);
497 0 : absoluteSplit = ratio * boundary;
498 : }
499 :
500 0 : absoluteSplit = LB_MAX(absoluteSplit, vp.y);
501 0 : absoluteSplit = LB_MIN(absoluteSplit, end);
502 :
503 0 : const float newPixelH = pvpH * node->split;
504 0 : const float oldPixelH = pvpH * node->oldsplit;
505 0 : if (int(fabs(newPixelH - oldPixelH)) < node->resistance2i.y())
506 : {
507 0 : absoluteSplit = vp.x + vp.w * node->oldsplit;
508 0 : node->split = node->oldsplit;
509 : }
510 : else
511 : {
512 0 : node->split = (absoluteSplit - vp.y) / vp.h;
513 0 : node->oldsplit = node->split;
514 : }
515 :
516 0 : LBLOG(LOG_LB2) << "Constrained split " << vp << " at X " << node->split
517 0 : << std::endl;
518 :
519 : // traverse children
520 0 : Viewport childVP = vp;
521 0 : childVP.h = (absoluteSplit - vp.y);
522 0 : _assign(node->left, childVP, range);
523 :
524 0 : childVP.y = childVP.getYEnd();
525 0 : childVP.h = end - childVP.y;
526 :
527 : // Fix 2994111: Rounding errors with 2D LB and 16 sources
528 : // Floating point rounding may create a width for the 'right'
529 : // child which is slightly below the parent width. Correct it.
530 0 : while (childVP.getYEnd() < end)
531 0 : childVP.h += std::numeric_limits<float>::epsilon();
532 :
533 0 : _assign(node->right, childVP, range);
534 0 : break;
535 : }
536 :
537 : case MODE_DB:
538 : {
539 0 : LBASSERT(vp == Viewport::FULL);
540 0 : const float end = range.end;
541 : float absoluteSplit =
542 0 : range.start + (range.end - range.start) * node->split;
543 :
544 0 : const float boundary(node->boundaryf);
545 0 : if (node->left->resources == 0.f)
546 0 : absoluteSplit = range.start;
547 0 : else if (node->right->resources == 0.f)
548 0 : absoluteSplit = end;
549 :
550 0 : const uint32_t ratio = uint32_t(absoluteSplit / boundary + .5f);
551 0 : absoluteSplit = ratio * boundary;
552 0 : if ((absoluteSplit - range.start) < boundary)
553 0 : absoluteSplit = range.start;
554 0 : if ((end - absoluteSplit) < boundary)
555 0 : absoluteSplit = end;
556 :
557 : const float oldSplit =
558 0 : range.start + (range.end - range.start) * node->oldsplit;
559 0 : if (fabs(absoluteSplit - oldSplit) < node->resistancef)
560 : {
561 0 : absoluteSplit = oldSplit;
562 0 : node->split = node->oldsplit;
563 : }
564 : else
565 : {
566 0 : node->split =
567 0 : (absoluteSplit - range.start) / (range.end - range.start);
568 0 : node->oldsplit = node->split;
569 : }
570 :
571 0 : LBLOG(LOG_LB2) << "Constrained split " << range << " at pos "
572 0 : << node->split << std::endl;
573 :
574 0 : Range childRange = range;
575 0 : childRange.end = absoluteSplit;
576 0 : _assign(node->left, vp, childRange);
577 :
578 0 : childRange.start = childRange.end;
579 0 : childRange.end = range.end;
580 0 : _assign(node->right, vp, childRange);
581 0 : break;
582 : }
583 :
584 : default:
585 0 : LBUNIMPLEMENTED;
586 : }
587 : }
588 :
589 0 : std::ostream& operator<<(std::ostream& os, const TreeEqualizer::Node* node)
590 : {
591 0 : if (!node)
592 0 : return os;
593 :
594 0 : os << lunchbox::disableFlush;
595 :
596 0 : if (node->compound)
597 0 : os << node->compound->getChannel()->getName() << " resources "
598 0 : << node->resources << " max size " << node->maxSize << std::endl;
599 : else
600 0 : os << "split " << node->mode << " @ " << node->split << " resources "
601 0 : << node->resources << " max size " << node->maxSize << std::endl
602 0 : << lunchbox::indent << node->left << node->right << lunchbox::exdent;
603 :
604 0 : os << lunchbox::enableFlush;
605 0 : return os;
606 : }
607 :
608 0 : std::ostream& operator<<(std::ostream& os, const TreeEqualizer* lb)
609 : {
610 0 : if (!lb)
611 0 : return os;
612 :
613 0 : os << lunchbox::disableFlush << "tree_equalizer" << std::endl
614 0 : << '{' << std::endl
615 0 : << " mode " << lb->getMode() << std::endl;
616 :
617 0 : if (lb->getDamping() != 0.5f)
618 0 : os << " damping " << lb->getDamping() << std::endl;
619 :
620 0 : if (lb->getBoundary2i() != Vector2i(1, 1))
621 0 : os << " boundary [ " << lb->getBoundary2i().x() << " "
622 0 : << lb->getBoundary2i().y() << " ]" << std::endl;
623 :
624 0 : if (lb->getBoundaryf() != std::numeric_limits<float>::epsilon())
625 0 : os << " boundary " << lb->getBoundaryf() << std::endl;
626 :
627 0 : if (lb->getResistance2i() != Vector2i(0, 0))
628 0 : os << " resistance [ " << lb->getResistance2i().x() << " "
629 0 : << lb->getResistance2i().y() << " ]" << std::endl;
630 :
631 0 : if (lb->getResistancef() != .0f)
632 0 : os << " resistance " << lb->getResistancef() << std::endl;
633 :
634 0 : os << '}' << std::endl << lunchbox::enableFlush;
635 0 : return os;
636 : }
637 : }
638 60 : }
|