Branch data Line data Source code
1 : : #ifdef RUN_TESTS
2 : : # include <gtest/gtest.h>
3 : : # include <algorithm>
4 : : # include <set>
5 : : #endif
6 : : /** @file rendering/window.cc
7 : : * @brief Defines Window, a renderer and manager of screen contents.
8 : : */
9 : :
10 : : #include <unordered_map>
11 : : #include <array>
12 : : #include <thread>
13 : :
14 : : #include "window.hh"
15 : : #include "color.hh"
16 : : #include "person.hh"
17 : : #include "clock.hh"
18 : : #include "ctype.hh"
19 : : #include "font_planner.hh"
20 : :
21 : : /** Generates an intensity table for the given combination of options:
22 : : * @param dim Dim symbols
23 : : * @param bold Bold symbols
24 : : * @param italic A value in range 0-1 representing the slant of the pixel row for italic text
25 : : */
26 : : static constexpr std::array<unsigned char,16> CalculateIntensityTable(bool dim,bool bold,float italic)
27 : : {
28 : : std::array<unsigned char,16> result={};
29 : : auto calc = [=](bool prev,bool cur,bool next) constexpr
30 : : {
31 : : float result = cur;
32 : : if(dim)
33 : : {
34 : : if(cur && !next)
35 : : {
36 : : if(prev) result *= float(1.f/3.f); // diminish rightmost pixel
37 : : else result *= float(2.f/3.f); // diminish all pixels
38 : : }
39 : : }
40 : : if(bold)
41 : : {
42 : : if(cur || prev) result += float(1.f/4.f); // add dim extra pixel, slightly brighten existing pixels
43 : : }
44 : : return result;
45 : : };
46 : : for(unsigned value=0; value<16; ++value)
47 : : {
48 : : bool values[4] = { bool(value&8), bool(value&4), bool(value&2), bool(value&1) }; // before,current,after,next
49 : : float thisresult = calc(values[0], values[1], values[2]);
50 : : float nextresult = calc(values[1], values[2], values[3]);
51 : : float factor = thisresult + (nextresult-thisresult)*italic;
52 : : // possible values of factor: 0, 1, 1.5, 0.333, 0.5
53 : : result[value] = int(factor*127 + 0.5f);
54 : : }
55 : : return result;
56 : : }
57 : :
58 : : /** Intensity tables for different combinations of font rendering.
59 : : *
60 : : * For taketables[mode][mask],
61 : : * mode is 16 when cell is dim
62 : : * plus 8 when cell is bold
63 : : * plus 0 when cell is not italic, a range from 0-7 when cell is italic (top to bottom)
64 : : * mask is a bitmask of four consecutive bits from the font, horizontally:
65 : : * bit 0 is previous
66 : : * bit 1 is current
67 : : * bit 2 is next
68 : : * bit 3 is next after next
69 : : * Result is a value in range 0-128,
70 : : * where 0 means fully background color,
71 : : * 128 means fully foreground color,
72 : : * and the rest of values are linearly interpolated between.
73 : : */
74 : : static constexpr std::array<unsigned char,16> taketables[] =
75 : : {
76 : : #define i(n,i) CalculateIntensityTable(n&2,n&1,i),
77 : : #define j(n) i(n,0/8.f)i(n,1/8.f)i(n,2/8.f)i(n,3/8.f)i(n,4/8.f)i(n,5/8.f)i(n,6/8.f)i(n,7/8.f)
78 : : j(0) j(1) j(2) j(3) j(4) j(5) j(6) j(7)
79 : : #undef j
80 : : #undef i
81 : : };
82 : :
83 : 269 : void Window::Render(std::size_t fx, std::size_t fy, std::uint32_t* pixels)
84 : : {
85 : : /* Blinking happens on timer. */
86 : 269 : unsigned timer = unsigned(GetTime() * 60.0);
87 : : /* Blink settings:
88 : : * blink1 = Used for blinking speed 1 (normal): 1.5 Hz with 50% duty (60/40)
89 : : * blink2 = Used for blinking speed 2 (fast blinking): 5 Hz with 50% duty (60/12)
90 : : * blink3 = Used for cursor blinking: 6 Hz with 50% duty (60/10)
91 : : */
92 : 269 : bool old_blink1 = (lasttimer/20)&1, cur_blink1 = (timer/20)&1;
93 : 269 : bool old_blink2 = (lasttimer/ 6)&1, cur_blink2 = (timer/ 6)&1;
94 : 269 : bool old_blink3 = lasttimer%10<5, cur_blink3 = timer%10<5;
95 : :
96 : : /* Fonts are cached for each 0x400 slots in Unicode */
97 : 269 : constexpr std::size_t font_granularity = 0x400;
98 : :
99 : : /* Cached fonts. Single-size and double-size. */
100 [ + + + - : 271 : static std::unordered_map<char32_t, FontPlan> fonts[2];
+ + ]
101 [ + + ]: 11665 : for(const auto& cell: cells)
102 : : {
103 [ + - ]: 11396 : unsigned dbl = isdouble(cell.ch) ? 1 : 0;
104 : 11396 : char32_t ch = cell.ch & ~(font_granularity-1);
105 [ - + ]: 11396 : fonts[dbl][ch].Create(dbl ? fx*2 : fx, fy, ch, font_granularity);
106 : : }
107 : :
108 : : #ifdef _OPENMP
109 : : std::size_t num_dirtycells = std::count_if(cells.begin(),cells.end(),
110 : : [](const Cell& c) { return c.dirty; });
111 : : double dirtiness = num_dirtycells / double(cells.size());
112 : : // 0.1: 4.17s (nt=4) 5.1s (nt=2) 3.8s (nt=8) 3.58s (nt=16) 8.0s (nt=48)
113 : : // 0.2: about 4s (nt=4)
114 : : // 0.5: 4.3s (nt=4)
115 : : // 1.0: 5.7s
116 : : unsigned thread_count = std::clamp(std::min(16u, unsigned(dirtiness*16/0.1)),
117 : : 1u, std::thread::hardware_concurrency());
118 : : #endif
119 : :
120 : 269 : std::size_t screen_width = fx*xsize;
121 : : #pragma omp parallel for schedule(static) collapse(2) num_threads(thread_count)
122 [ + + ]: 1126 : for(std::size_t y=0; y<ysize; ++y) // cell-row
123 [ + + ]: 9649 : for(std::size_t fr=0; fr<fy; ++fr) // font-row
124 : : {
125 [ + + - + ]: 8792 : bool is_person_row = cells[y*xsize+0].inverse && cells[y*xsize+xsize-1].inverse;
126 : :
127 : 8792 : std::uint32_t* pix = pixels + (y*fy+fr)*screen_width;
128 : 8792 : std::size_t xroom = xsize;
129 : 8792 : bool cursor_seen = !cursorvis;
130 [ + + + - ]: 8792 : if(y != cursy && y != lastcursy) cursor_seen = true;
131 [ + + ]: 138320 : for(std::size_t x=0; x<xroom; ++x) // cell-column
132 : : {
133 [ + + ]: 129528 : auto& cell = cells[y * xsize + x];
134 : :
135 : 129528 : unsigned xscale = 1;
136 [ + + + - ]: 129528 : if(cell.render_size && (x+1) < xroom) { xscale = 2; --xroom; }
137 : 129528 : unsigned width = fx * xscale;
138 : :
139 : 129528 : auto char_to_render = cell.ch;
140 : 129528 : bool was_double = isdouble(char_to_render);
141 [ - + ]: 129528 : if(was_double) width *= 2;
142 : :
143 [ + + + + : 129528 : bool cursorloc = y == cursy && (x == cursx || (was_double && cursx == x+1));
- + - - ]
144 [ - + - - : 129528 : bool prev_cursorloc = y == lastcursy && (x == lastcursx || (was_double && lastcursx == x+1));
- - - - ]
145 : :
146 [ - + ]: 129528 : if(!cell.dirty
147 [ # # ]: 0 : && !is_person_row
148 [ # # ]: 0 : && (!(cursorloc || prev_cursorloc)
149 [ # # ]: 0 : || (cursorloc == prev_cursorloc && old_blink3 == cur_blink3)
150 : : )
151 [ # # # # ]: 0 : && (cell.blink!=1 || old_blink1 == cur_blink1)
152 [ # # # # ]: 0 : && (cell.blink!=2 || old_blink2 == cur_blink2)
153 : : )
154 : : {
155 : 0 : pix += width;
156 : 0 : x += was_double;
157 : 0 : continue;
158 : : }
159 : :
160 : 129528 : unsigned fr_actual = fr;
161 [ + + + ]: 129528 : switch(cell.render_size)
162 : : {
163 : : default: break;
164 : 80 : case 2: fr_actual /= 2; break;
165 : 200 : case 3: fr_actual /= 2; fr_actual += fy/2; break;
166 : : }
167 : :
168 [ + + ]: 8 : bool line = (cell.underline && (fr == (fy-1)))
169 [ + + + + : 129527 : || (cell.underline2 && (fr == (fy-1) || fr == (fy-3)))
+ + ]
170 [ + + + + ]: 129525 : || (cell.overstrike && (fr == (fy/2)))
171 [ + + + + : 259052 : || (cell.overlined && (fr == 0));
+ + ]
172 : :
173 : 129528 : bool do_cursor = !cursor_seen && cursorloc;
174 [ + + ]: 129528 : if(do_cursor)
175 : : {
176 : 608 : unsigned lim = 7;
177 [ - + ]: 608 : if(cells[4].ch == U'O') lim = 3;
178 [ + + + + ]: 608 : if(fr >= fy*lim/8 && cur_blink3)
179 : : {}
180 : : else
181 : 564 : do_cursor = false;
182 : : cursor_seen = true;
183 : : }
184 : :
185 : 129528 : bool is_bold = true;
186 : 129528 : unsigned long widefont = 0;
187 [ + + + + ]: 129528 : /**/ if(cell.blink == 1 && !cur_blink1) {}
188 [ + + + + ]: 129192 : else if(cell.blink == 2 && !cur_blink2) {}
189 [ + - ]: 257872 : else { auto r = fonts[was_double ? 1 : 0].find(char_to_render &~ (font_granularity-1))->second
190 : 128936 : .LoadGlyph(char_to_render % font_granularity, fr_actual, width);
191 : 128936 : widefont = r.bitmap;
192 : 128936 : is_bold = r.bold; }
193 : :
194 : 129528 : unsigned shift = 2 + !cell.italic;
195 [ + + - + ]: 129528 : bool dim = cell.dim && is_bold; // Disable dim on non-bold fonts, because it makes them look bad.
196 : :
197 : 129528 : const unsigned mode = cell.italic*(fr*8/fy)
198 : 129528 : + 8*cell.bold
199 : 129528 : + 16*dim;
200 : :
201 [ + + ]: 1168632 : for(std::size_t fc=0; fc<width; ++fc)
202 : : {
203 : 1039104 : auto fg = cell.fgcolor;
204 : 1039104 : auto bg = cell.bgcolor;
205 : :
206 [ + + ]: 1039104 : if(cell.inverse ^ inverse)
207 : : {
208 : 619584 : std::swap(fg, bg);
209 : : }
210 : :
211 : : // taketables requires four consecutive bits from the font:
212 : : // previous, current, next and next2.
213 : : // <<2 is so that we can get previous pixel, and also not do -1 in width-1-fc
214 : :
215 : 1039104 : unsigned mask = ((widefont << shift) >> (width - fc)) & 0xF;
216 [ + + ]: 1039104 : int take = taketables[mode][mask];
217 [ + + ]: 1039104 : unsigned untake = std::max(0,128-take);
218 : 1039104 : unsigned pre_bg = bg, pre_fg = fg;
219 [ + + ]: 1039104 : if(cell.inverse)
220 : : {
221 : 619584 : PersonTransform(bg,fg, xsize*fx, x*fx+fc, y*fy+fr,
222 [ + + ]: 619584 : (is_person_row && y==0) ? 1
223 [ + + ]: 309824 : : y == (ysize-1) ? 2
224 : : : 0);
225 : : }
226 [ + + ]: 1039104 : if(do_cursor)
227 : : {
228 : 352 : unsigned curs = cursorcolor;
229 [ - + ]: 352 : if(curs == bg) curs = fg;
230 : 352 : fg = bg;
231 : 352 : bg = curs;
232 : : }
233 : 1039104 : unsigned color = Mix(bg,fg, untake, take, 128);
234 : :
235 [ + + - + : 1039104 : if(line && take == 0 && (!cell.inverse || color != 0x000000))
- - ]
236 : : {
237 : 78 : auto brightness = [](unsigned rgb)
238 : : {
239 : 52 : auto p = Unpack(rgb);
240 : 52 : return p[0]*299 + p[1]*587 + p[2]*114;
241 : : };
242 [ - + ]: 26 : if(brightness(pre_fg) < brightness(pre_bg))
243 : 0 : color = Mix(0x000000, color, 1,1,2);
244 : : else
245 : 26 : color = Mix(0xFFFFFF, color, 1,1,2);
246 : : }
247 : :
248 : : /*if(color && use_fx < fx)
249 : : {
250 : : // Make brighter to compensate for black 9th column
251 : : color = Mix(0xFFFFFF, color, fx-use_fx,use_fx, fx);
252 : : }*/
253 : :
254 : 1039104 : pix[fc] = color;
255 : : }
256 : 129528 : pix += width;
257 : 129528 : x += was_double;
258 : : }
259 : : }
260 : :
261 [ + + ]: 1126 : for(std::size_t y=0; y<ysize; ++y) // cell-row
262 [ + + ]: 12253 : for(std::size_t x=0; x<xsize; ++x) // cell-column
263 : 11396 : cells[y * xsize + x].dirty = false;
264 : :
265 : 269 : lastcursx = cursx;
266 : 269 : lastcursy = cursy;
267 : 269 : lasttimer = timer;
268 : 269 : }
269 : :
270 : 2 : void Window::Resize(std::size_t newsx, std::size_t newsy)
271 : : {
272 : 2 : std::vector<Cell> newcells(newsx*newsy, blank);
273 [ - + + + ]: 31 : for(std::size_t my=std::min(ysize, newsy), y=0; y<my; ++y)
274 [ + + + + ]: 1070 : for(std::size_t mx=std::min(xsize, newsx), x=0; x<mx; ++x)
275 : 1016 : newcells[x + y*newsx] = cells[x + y*xsize];
276 : :
277 : 2 : cells = std::move(newcells);
278 : 2 : xsize = newsx;
279 : 2 : ysize = newsy;
280 : 2 : Dirtify();
281 [ - + ]: 2 : if(cursy >= ysize) cursy = ysize-1;
282 [ - + ]: 2 : if(cursx >= xsize) cursx = xsize-1;
283 : 2 : }
284 : :
285 : 674 : void Window::Dirtify()
286 : : {
287 : 674 : lastcursx = lastcursy = ~std::size_t();
288 : 674 : lasttimer = ~0u;
289 [ + + ]: 710899 : for(auto& c: cells) c.dirty = true;
290 : 674 : }
291 : :
292 : 11 : void Window::LineSetRenderSize(unsigned val)
293 : : {
294 [ + + ]: 401 : for(std::size_t x=0; x<xsize; ++x)
295 : 390 : cells[cursy*xsize+x].render_size = val;
296 : 11 : }
297 : :
298 : : #ifdef RUN_TESTS
299 : 3 : TEST(window, window_dirty)
300 : : {
301 : 1 : std::cout << "This test will take some time (will load 8x8 fonts for rendering)\n";
302 : 1 : Window w(80, 25);
303 : : // Initially dirty
304 [ - + ]: 1 : w.cells[0].ch = 'X';
305 : 501 : EXPECT_TRUE( std::all_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.dirty;}) );
306 : : // Clean after render
307 [ + - ]: 2 : std::vector<std::uint32_t> buffer(80*8 * 25*8);
308 [ + - ]: 1 : w.Render(8,8, &buffer[0]);
309 : 501 : EXPECT_TRUE( std::none_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.dirty;}) );
310 : : // PutCh dirtifies some but not all cells
311 [ + - ]: 1 : w.PutCh(0,0, U'A');
312 : 2 : EXPECT_FALSE( std::none_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.dirty;}) );
313 : 2 : EXPECT_FALSE( std::all_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.dirty;}) );
314 : : // Dirty after resize
315 [ + - ]: 1 : w.Resize(40, 25);
316 : 252 : EXPECT_TRUE( std::all_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.dirty;}) );
317 : 1 : }
318 : 3 : TEST(window, fillbox)
319 : : {
320 : 1 : Window w(80, 25);
321 : : // Initially blank
322 : 501 : EXPECT_TRUE( std::all_of(w.cells.begin(), w.cells.end(), [](Cell c){return c.ch == U' ';}) );
323 : : // Fillbox with 'A' fills with 'A'
324 : 1 : Cell a; a.ch = U'A';
325 : 1 : w.FillBox(20,20, 40,3, a);
326 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 40*3 );
327 : : // Fillbox without character fills with space
328 : 1 : w.FillBox(30,21, 20,1);
329 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 40*3-20 );
330 : 2002 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == w.blank;}), 80*25-(40*3-20) );
331 : 1 : }
332 : 3 : TEST(window, copytext)
333 : : {
334 : 1 : Window w(80, 25);
335 : : // Create 15x10 'A' box
336 : 1 : Cell a; a.ch = U'A';
337 : 1 : w.FillBox(20,10, 15,10, a);
338 : : // Copy it
339 : 1 : w.CopyText(60,14, 20,10, 15,10);
340 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 2*15*10 );
341 : : // Test overlap (left) -- should add 10
342 : 1 : w.CopyText(19,10, 20,10, 15,10);
343 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 2*15*10 + 10 );
344 : : // Test overlap (right) -- should add 10
345 : 1 : w.CopyText(21,10, 20,10, 15,10);
346 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 2*15*10 + 10+10 );
347 : : // Test overlap (above) -- should add 15
348 : 1 : w.CopyText(20,9, 20,10, 15,10);
349 : 2001 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 2*15*10 + 10+10+15 );
350 : : // Test overlap (below) -- should add 15
351 : 1 : w.CopyText(20,11, 20,10, 15,10);
352 : 2002 : EXPECT_EQ( std::count_if(w.cells.begin(), w.cells.end(), [&](Cell c){return c == a;}), 2*15*10 + 10+10+15+15 );
353 : 1 : }
354 : 3 : TEST(window, font_styles_are_distinct)
355 : : {
356 : : // Stop timer, so that we do not get blinking cursor
357 : 1 : SetTimeFactor(0.);
358 : 1 : Window w(10,10);
359 : 1 : const std::size_t fx=8, fy=8, npixels = w.xsize*fx * w.ysize*fy;
360 : : // Place 'B' at (4,4)
361 [ + - ]: 1 : w.PutCh(4,4, U'B');
362 : 1 : unsigned position = 4 * w.xsize + 4;
363 : : // Backup that cell
364 [ + - ]: 1 : Cell c = w.cells[position];
365 : : // Create model rendering
366 [ + - ]: 1 : std::vector<std::uint32_t> model_rendering(npixels);
367 [ + - ]: 1 : w.Render(fx,fy, &model_rendering[0]);
368 : : // Render in different styles
369 : 11 : auto makestyle = [&](auto&& mogrify)
370 : : {
371 : 10 : std::vector<std::uint32_t> pix(npixels);
372 : 10 : w.Dirtify();
373 [ + - ]: 10 : w.cells[position]=c;
374 : 10 : mogrify(w.cells[position]);
375 : 10 : w.Render(fx,fy, &pix[0]);
376 : 10 : return pix;
377 : 1 : };
378 [ + - ]: 2 : auto bold = makestyle([&](Cell&t) { t.bold=true; });
379 [ + - ]: 2 : auto dim = makestyle([&](Cell&t) { t.dim=true; });
380 [ + - ]: 2 : auto underline = makestyle([&](Cell&t) { t.underline=true; });
381 [ + - ]: 2 : auto underline2 = makestyle([&](Cell&t) { t.underline2=true; });
382 [ + - ]: 2 : auto italic = makestyle([&](Cell&t) { t.italic=true; });
383 [ + - ]: 2 : auto inverse = makestyle([&](Cell&t) { t.inverse=true; });
384 [ + - ]: 2 : auto overstrike = makestyle([&](Cell&t) { t.overstrike=true; });
385 [ + - ]: 2 : auto overlined = makestyle([&](Cell&t) { t.overlined=true; });
386 [ + - ]: 2 : auto whitefg = makestyle([&](Cell&t) { t.fgcolor=0xFFFFFF; });
387 [ + - ]: 2 : auto whitebg = makestyle([&](Cell&t) { t.bgcolor=0xFFFFFF; });
388 : 1 : EXPECT_NE(model_rendering, bold);
389 : 1 : EXPECT_NE(model_rendering, dim);
390 : 1 : EXPECT_NE(model_rendering, underline);
391 : 1 : EXPECT_NE(model_rendering, underline2);
392 : 1 : EXPECT_NE(model_rendering, italic);
393 : 1 : EXPECT_NE(model_rendering, inverse);
394 : 1 : EXPECT_NE(model_rendering, overstrike);
395 : 1 : EXPECT_NE(model_rendering, overlined);
396 : 1 : EXPECT_NE(model_rendering, whitefg);
397 : 1 : EXPECT_NE(model_rendering, whitebg);
398 : 3 : EXPECT_NE(bold, dim); EXPECT_NE(bold, underline); EXPECT_NE(bold, italic);
399 : 3 : EXPECT_NE(bold, inverse); EXPECT_NE(bold, overstrike); EXPECT_NE(bold, overlined);
400 : 3 : EXPECT_NE(bold, whitefg); EXPECT_NE(bold, whitebg); EXPECT_NE(bold, underline2);
401 : 2 : EXPECT_NE(dim, underline); EXPECT_NE(dim, italic);
402 : 3 : EXPECT_NE(dim, inverse); EXPECT_NE(dim, overstrike); EXPECT_NE(dim, overlined);
403 : 3 : EXPECT_NE(dim, whitefg); EXPECT_NE(dim, whitebg); EXPECT_NE(dim, underline2);
404 : 2 : EXPECT_NE(underline, italic); EXPECT_NE(underline, underline2);
405 : 3 : EXPECT_NE(underline, inverse); EXPECT_NE(underline, overstrike); EXPECT_NE(underline, overlined);
406 : 2 : EXPECT_NE(underline, whitefg); EXPECT_NE(underline, whitebg);
407 : 3 : EXPECT_NE(italic, inverse); EXPECT_NE(italic, overstrike); EXPECT_NE(italic, overlined);
408 : 3 : EXPECT_NE(italic, whitefg); EXPECT_NE(italic, whitebg); EXPECT_NE(italic, underline2);
409 : 4 : EXPECT_NE(inverse, overstrike); EXPECT_NE(inverse, overlined); EXPECT_NE(inverse, whitefg); EXPECT_NE(inverse, whitebg);
410 : 3 : EXPECT_NE(overstrike, overlined); EXPECT_NE(overstrike, whitefg); EXPECT_NE(overstrike, whitebg);
411 : 3 : EXPECT_NE(overlined, whitefg); EXPECT_NE(overlined, whitebg); EXPECT_NE(inverse, underline2);
412 : 5 : EXPECT_NE(whitefg, whitebg); EXPECT_NE(overstrike, underline2); EXPECT_NE(whitefg, underline2); EXPECT_NE(whitebg, underline2);
413 : 1 : }
414 : 3 : TEST(window, rendersize)
415 : : {
416 : 1 : SetTimeFactor(1.);
417 : 1 : Window w(10,4);
418 : 1 : w.cursorvis = false; // Make cursor invisible
419 : 1 : const std::size_t fx=8, fy=8, ncells=w.xsize*w.ysize, npixels = fx*fy*ncells;
420 : :
421 : : // Place 'X' at (4,2).
422 : : // This symbol is chosen because its top half looks like V and bottom half like Λ.
423 : : // This makes it easy to detect programmatically whether we have rendered it correctly.
424 [ + - ]: 1 : w.PutCh(4,2, U'X');
425 : :
426 : : // Create model rendering
427 [ + - + - ]: 1 : std::vector<std::uint32_t> model_rendering(npixels), pixels(npixels);
428 [ + - ]: 1 : w.Render(fx,fy, &model_rendering[0]);
429 : :
430 : 8 : auto makestyle = [&](auto&& mogrify)
431 : : {
432 : 7 : std::vector<std::uint32_t> pix(npixels);
433 : 7 : w.Dirtify();
434 [ + - ]: 7 : mogrify(w);
435 [ + - ]: 7 : w.Render(fx,fy, &pix[0]);
436 : : /*unsigned xpix = fx*w.xsize, ypix=fy*w.ysize;
437 : : for(unsigned p=0,y=0; y<ypix; ++y)
438 : : {
439 : : for(unsigned x=0; x<xpix; ++x,++p)
440 : : std::cout << (pix[p] ? '#' : ' ');
441 : : std::cout << '\n';
442 : : }*/
443 : 7 : return pix;
444 : 1 : };
445 : : // Since cursor is at (0,0), check that LineSetRenderSize does nothing
446 : 1 : EXPECT_EQ(w.cursx, 0u);
447 : 1 : EXPECT_EQ(w.cursy, 0u);
448 : 3 : EXPECT_EQ(model_rendering, makestyle([](auto&w){ w.LineSetRenderSize(0); }));
449 : 3 : EXPECT_EQ(model_rendering, makestyle([](auto&w){ w.LineSetRenderSize(1); }));
450 : 3 : EXPECT_EQ(model_rendering, makestyle([](auto&w){ w.LineSetRenderSize(2); }));
451 : 3 : EXPECT_EQ(model_rendering, makestyle([](auto&w){ w.LineSetRenderSize(3); }));
452 : :
453 : 5 : auto analyze_shape = [&](auto&& pixels)
454 : : {
455 : : // Find the first and last rows where something is drawn
456 : : // Find the first and last pixel on both rows
457 : 4 : unsigned first_row=~0u, first_ranges[2]{~0u,0};
458 : 4 : unsigned last_row=~0u, last_ranges[2]{~0u,0};
459 : 4 : unsigned xpix = fx*w.xsize, ypix=fy*w.ysize;
460 [ + + ]: 132 : for(unsigned p=0,y=0; y<ypix; ++y)
461 : : {
462 : : bool first = false;
463 [ + + ]: 10368 : for(unsigned x=0; x<xpix; ++x,++p)
464 [ + + ]: 10240 : if(pixels[p])
465 : : {
466 [ + + ]: 189 : if(first_row == ~0u) first = true;
467 [ + + ]: 185 : if(first)
468 : : {
469 [ + + ]: 28 : if(first_ranges[0] == ~0u) { first_row = y; first_ranges[0] = x; }
470 : 28 : first_ranges[1] = x;
471 : : }
472 [ + + ]: 189 : if(last_row != y) { last_ranges[0] = x; last_row = y; }
473 : 189 : last_ranges[1] = x;
474 : : }
475 : : }
476 : : return std::array<std::size_t,6>{
477 : 4 : first_row, last_row,
478 : 4 : first_ranges[0], first_ranges[1],
479 : 4 : last_ranges[0], last_ranges[1]};
480 : 1 : };
481 : :
482 : 1 : auto model_shape = analyze_shape(model_rendering);
483 : 1 : w.cursx = 4;
484 : 1 : w.cursy = 2;
485 : :
486 : : // First, make sure that in the model rendering,
487 : : // the symbol is strictly within permitted bounds
488 : 2 : EXPECT_GE(model_shape[0], w.cursy*fy); EXPECT_LT(model_shape[1], (w.cursy+1)*fy);
489 : 2 : EXPECT_GE(model_shape[2], w.cursx*fx); EXPECT_LT(model_shape[3], (w.cursx+1)*fx);
490 : 2 : EXPECT_GE(model_shape[4], w.cursx*fx); EXPECT_LT(model_shape[5], (w.cursx+1)*fx);
491 : : // Symbol 'X' height should be at least 50% of cell height
492 : 1 : EXPECT_GE(model_shape[1], model_shape[0] + fy*2/4);
493 : : // Symbol 'X' width at top should be at least 75% of cell width
494 : 1 : EXPECT_GE(model_shape[3], model_shape[2] + fx*3/4);
495 : : // Symbol 'X' width at bottom should be at least 75% of cell width
496 : 1 : EXPECT_GE(model_shape[5], model_shape[4] + fx*3/4);
497 : :
498 : : // Size 1: Double width, full symbol (X); height stays the same.
499 [ + - ]: 2 : auto shape1 = analyze_shape( makestyle([](auto&w){ w.LineSetRenderSize(1); }) );
500 : 2 : EXPECT_EQ(shape1[0], model_shape[0]); EXPECT_EQ(shape1[1], model_shape[1]); // Height should be identical
501 : 2 : EXPECT_EQ(shape1[2], model_shape[2]*2); EXPECT_EQ(shape1[3], model_shape[3]*2+1); // Width should be double
502 : 2 : EXPECT_EQ(shape1[4], model_shape[4]*2); EXPECT_EQ(shape1[5], model_shape[5]*2+1); // Width should be double
503 : :
504 : : // Size 2: Double width, only top half (V) is displayed at double size
505 [ + - ]: 2 : auto shape2 = analyze_shape( makestyle([](auto&w){ w.LineSetRenderSize(2); }) );
506 : 2 : EXPECT_GE(shape2[0], w.cursy*fy); EXPECT_LT(shape2[1], (w.cursy+1)*fy); // Y coordinates should be in right bounds
507 : 2 : EXPECT_EQ(shape2[2], shape1[2]); EXPECT_EQ(shape2[3], shape1[3]); // Top line should be identical to shape1
508 : 2 : EXPECT_GT(shape2[4], shape1[4]); EXPECT_LT(shape2[5], shape1[5]); // Bottom line should be narrower.
509 : :
510 : : // Size 2: Double width, only bottom half (Λ) is displayed at double size
511 [ + - ]: 2 : auto shape3 = analyze_shape( makestyle([](auto&w){ w.LineSetRenderSize(3); }) );
512 : 2 : EXPECT_GE(shape3[0], w.cursy*fy); EXPECT_LT(shape3[1], (w.cursy+1)*fy); // Y coordinates should be in right bounds
513 : 2 : EXPECT_GT(shape3[2], shape1[2]); EXPECT_LT(shape3[3], shape1[3]); // Top line should be narrower
514 : 3 : EXPECT_EQ(shape3[4], shape1[4]); EXPECT_EQ(shape3[5], shape1[5]); // Bottom line should be identical to shape1.
515 : 1 : }
516 : 3 : TEST(window, cursor_blinks)
517 : : {
518 : 1 : SetTimeFactor(0.);
519 : 1 : Window w(10,4);
520 : 1 : w.cursorvis = true; // Make cursor visible
521 : :
522 : 1 : const std::size_t fx=8, fy=8, cell_pixels = fx*fy, npixels = cell_pixels * w.xsize*w.ysize;
523 : :
524 : : // Render 1 second at 64 fps, count frames where cursor is visible
525 : 1 : unsigned count_cursor_visible = 0, count_frames = 0;
526 [ + + ]: 65 : for(unsigned frame=0; frame<64; ++frame, ++count_frames)
527 : : {
528 [ + - ]: 64 : AdvanceTime(1.0 / 64.0);
529 [ + - ]: 64 : std::vector<std::uint32_t> pix(npixels);
530 : 64 : w.Dirtify();
531 [ + - ]: 64 : w.Render(fx,fy, &pix[0]);
532 : 64 : unsigned visible_pixels = std::count_if(pix.begin(), pix.end(), [](auto c){return c; });
533 : : // Cursor size should be less than 20% of cell size
534 : 64 : EXPECT_LT(visible_pixels, cell_pixels/5);
535 [ + + ]: 64 : if(visible_pixels) ++count_cursor_visible;
536 : 64 : }
537 : : // Cursor should be visible at least 25% of frames
538 : 1 : EXPECT_GT(count_cursor_visible, count_frames*1/4);
539 : : // Cursor should be invisible at least 25% of frames
540 : 2 : EXPECT_LT(count_cursor_visible, count_frames*3/4);
541 : 1 : }
542 : 3 : TEST(window, text_blinks)
543 : : {
544 : 1 : SetTimeFactor(0.);
545 : 1 : Window w(3,3);
546 : 1 : w.cursorvis = false; // Make cursor invisible
547 [ + - ]: 1 : w.blank.blink = 0; w.PutCh(0,0, U'A');
548 [ + - ]: 1 : w.blank.blink = 1; w.PutCh(1,0, U'B');
549 [ + - ]: 1 : w.blank.blink = 2; w.PutCh(2,0, U'C');
550 : 1 : const std::size_t fx=8, fy=8, cell_pixels = fx*fy, npixels = cell_pixels * w.xsize*w.ysize;
551 : : // Render 1 second at 64 fps
552 [ + + ]: 65 : for(unsigned frame=0; frame<64; ++frame)
553 : : {
554 [ + - ]: 64 : AdvanceTime(1.0 / 64.0);
555 [ + - ]: 64 : std::vector<std::uint32_t> pix(npixels);
556 : 64 : w.Dirtify();
557 [ + - ]: 64 : w.Render(fx,fy, &pix[0]);
558 : 64 : }
559 : 1 : }
560 : 3 : TEST(window, person_animation)
561 : : {
562 : : // Let window width be 20 cells, font height 16 pixels.
563 : : // Render some text in top row in inverse.
564 : 1 : static const char32_t text[] = U"This is sample text ";
565 : 1 : const unsigned width = sizeof(text)/sizeof(*text)-1, height = 2;
566 : 1 : const unsigned fx = 8, fy = 16, time = 30, fps = 4, npixels=width*height*fx*fy;
567 : 1 : SetTimeFactor(0.); // Chose manual timer
568 : 1 : Window w(width, height);
569 : 1 : w.cursorvis = false; // Make cursor invisible
570 : 1 : w.blank.inverse = true; // Choose inverse attribute
571 [ + + + - : 22 : for(auto c: text) if(c) { w.PutCh(w.cursx, height-1, c); w.PutCh(w.cursx++, w.cursy, c); }
+ - + + ]
572 : 1 : EXPECT_EQ(w.cursx, width);
573 : :
574 : : // Wait until Person's starting X coordinate is outside window boundaries
575 [ + - + + ]: 102 : while(PersonBaseX(width*fx) < int(width*fx))
576 [ + - ]: 101 : AdvanceTime(0.01);
577 : :
578 [ + - ]: 2 : std::vector<std::uint32_t> model(npixels);
579 [ + - ]: 1 : w.Render(fx,fy, &model[0]);
580 : :
581 : : /*{unsigned xpix = fx*w.xsize, ypix=fy*w.ysize;
582 : : for(unsigned p=0,y=0; y<fy; ++y)
583 : : {
584 : : for(unsigned x=0; x<xpix; ++x,++p)
585 : : {
586 : : auto q = Unpack(model[p]);
587 : : char Buf[32]; std::sprintf(Buf, "%X%X%X", q[0]>>4,q[1]>>4,q[2]>>4);
588 : : std::cout << Buf;
589 : : }
590 : : std::cout << '\n';
591 : : }}*/
592 : :
593 : : // Re-render screen for 30 seconds at 4 fps.
594 : : // Every frame, the following conditions must be true:
595 : : // - Most of the text is visible
596 : : // - At most 16x16 pixels may be changed at any given time
597 : : // - The list of first changed columns should not be constant
598 : 2 : std::set<unsigned> change_columns;
599 [ + + ]: 121 : for(unsigned frame=0; frame<time*fps; ++frame)
600 : : {
601 : 120 : const unsigned allowed_diff_x = 16, allowed_diff_y = 16;
602 : :
603 [ + - ]: 120 : AdvanceTime(1.0 / fps);
604 [ + - ]: 120 : std::vector<std::uint32_t> pix(npixels);
605 : 120 : w.Dirtify();
606 [ + - ]: 120 : w.Render(fx,fy, &pix[0]);
607 : 120 : unsigned differences = 0, diffy[2]={~0u,0u}, diffx[2]={~0u,0u}, xpix=width*fx;
608 : :
609 : : /*{unsigned xpix = fx*w.xsize, ypix=fy*w.ysize;
610 : : for(unsigned p=0,y=0; y<fy; ++y)
611 : : {
612 : : for(unsigned x=0; x<xpix; ++x,++p)
613 : : {
614 : : auto q = Unpack(pix[p]);
615 : : char Buf[32]; std::sprintf(Buf, "%X%X%X", q[0]>>4,q[1]>>4,q[2]>>4);
616 : : std::cout << Buf;
617 : : }
618 : : std::cout << '\n';
619 : : }}*/
620 : :
621 [ + + ]: 307320 : for(unsigned n=0; n<xpix*fy; ++n)
622 [ + + ]: 307200 : if(pix[n] != model[n])
623 : : {
624 : 15210 : ++differences;
625 [ + + ]: 15210 : diffy[0] = std::min(diffy[0], n / xpix);
626 [ + + ]: 15210 : diffy[1] = std::max(diffy[1], n / xpix);
627 [ + + ]: 15210 : diffx[0] = std::min(diffx[0], n % xpix);
628 [ + + ]: 16073 : diffx[1] = std::max(diffx[1], n % xpix);
629 : : }
630 : 120 : EXPECT_LT(differences, npixels - allowed_diff_x*allowed_diff_y);
631 : : //printf("Diffs: y(%d..%d) x(%d..%d)\n", diffy[0],diffy[1], diffx[0],diffx[1]);
632 [ + + ]: 120 : if(differences)
633 : : {
634 : 91 : EXPECT_LE(diffy[1]-diffy[0], allowed_diff_y);
635 : 91 : EXPECT_LE(diffx[1]-diffx[0], allowed_diff_x);
636 [ + - ]: 91 : change_columns.insert(diffx[0]);
637 : : }
638 : 120 : }
639 : : // Expect the person to appear in at least 10 different positions
640 : 2 : EXPECT_GT(change_columns.size(), std::size_t(10));
641 : 1 : }
642 : 3 : TEST(window, coverage)
643 : : {
644 : : // For coverage, run Cell comparisons
645 : 1 : Window w(10, 10);
646 : 1 : Cell mod; mod.bold = true;
647 : 1 : EXPECT_TRUE(w.blank == w.blank);
648 : 1 : EXPECT_FALSE(w.blank != w.blank);
649 : 1 : EXPECT_FALSE(w.blank == mod);
650 : 2 : EXPECT_TRUE(w.blank != mod);
651 : 1 : }
652 : : #endif
|