1 /*
2  * Copyright 2017-2019, Björn Ståhl
3  * Description: CLI image file viewer
4  * License: 3-Clause BSD, see COPYING file in arcan source repository
5  * Reference: http://arcan-fe.com, README.MD
6  */
7 #include <arcan_shmif.h>
8 #include <arcan_tuisym.h>
9 #include <unistd.h>
10 #include <inttypes.h>
11 #include <poll.h>
12 #include <getopt.h>
13 #include <stdarg.h>
14 #include "imgload.h"
15 
16 static void progress_report(float progress);
17 
18 #define AR_EPSILON 0.001
19 #define STBIR_PROGRESS_REPORT(val) progress_report(val)
20 #define STB_IMAGE_RESIZE_IMPLEMENTATION
21 #include "stb_image_resize.h"
22 
23 /*
24  * shared global state with imgloader in liu of a config- call for now
25  */
26 int image_size_limit_mb = 64;
27 bool disable_syscall_flt = false;
28 
29 /*
30  * all the context needed for one window, could theoretically be used for
31  * multiple windows with different playlists etc. stereo mode is a bit
32  * special as the state contains two target outputs
33  */
34 struct draw_state {
35 	struct arcan_shmif_cont* con;
36 
37 /*
38  * stereoscopic rendering may need an extra context and synch signalling
39  * so that updates are synched to updates on the right eye.
40  */
41 	struct arcan_shmif_cont* con_right;
42 	bool stereo;
43 
44 /* blitting controls */
45 	shmif_pixel pad_col;
46 	int out_w, out_h;
47 	bool aspect_ratio;
48 	bool source_size;
49 	int blit_ind;
50 	float dpi;
51 
52 /* loading/ resource management state */
53 	int wnd_lim, wnd_fwd, wnd_pending, wnd_act;
54 	int wnd_prev, wnd_next;
55 	int timeout;
56 	bool stdin_pending, loaded, animated, vector;
57 
58 /* playlist related state */
59 	struct img_state* playlist;
60 	int pl_ind, pl_size;
61 	int step_timer, init_timer;
62 	bool step_block;
63 	bool handover_exec;
64 
65 /* user- navigation related states */
66 	bool block_input;
67 	bool loop;
68 
69 /* dirty requested as triggered from input handlers */
70 	bool dirty;
71 };
72 
73 static struct draw_state* last_ds;
74 static bool set_playlist_pos(struct draw_state* ds, int new_i);
75 
debug_message(const char * msg,...)76 void debug_message(const char* msg, ...)
77 {
78 #ifdef DEBUG
79 	va_list args;
80 	va_start( args, msg );
81 		vfprintf(stderr,  msg, args );
82 	va_end( args);
83 #endif
84 }
85 
blit(struct arcan_shmif_cont * dst,const struct img_data * const src,const struct draw_state * const state)86 static void blit(struct arcan_shmif_cont* dst,
87 	const struct img_data* const src, const struct draw_state* const state)
88 {
89 /* draw pad color if the active image is incorrect */
90 	if (!src || !src->ready){
91 		for (int row = 0; row < dst->h; row++)
92 			for (int col = 0; col < dst->h; col++)
93 				dst->vidp[row * dst->pitch + col] = state->pad_col;
94 		arcan_shmif_signal(dst, SHMIF_SIGVID);
95 		return;
96 	}
97 
98 /* resize to match? try and find the largest permitted fit - this will be
99  * SHMPAGE_MAXW * SHMPAGE_MAXH (in bytes) but can theoretically be larger in
100  * certain circumstances and practically be smaller depending on what the
101  * server end says */
102 	if (state->source_size){
103 		size_t dw = src->w;
104 		size_t dh = src->h;
105 
106 		int attempts = 0;
107 		while (dw && dh && !arcan_shmif_resize(dst, dw, dh)){
108 			debug_message("resize to %zu*%zu rejected, trying %zu*%zu\n");
109 			if (!attempts){
110 				if (dw > dh){
111 					float ratio = (float)dh / (float)dw;
112 					dw = PP_SHMPAGE_MAXW;
113 					dh = dw * ratio;
114 					attempts++;
115 				}
116 				else {
117 					float ratio = (float)dw / (float)dh;
118 					dh = PP_SHMPAGE_MAXH;
119 					dw = dh * ratio;
120 					attempts++;
121 				}
122 				continue;
123 			}
124 			dw >>= 1;
125 			dh >>= 1;
126 			attempts++;
127 		}
128 	}
129 /* safe: shmif_resize on <= 0 and <= 0 would fail without changing dst->w,h */
130 	int dw = dst->w;
131 	int dh = dst->h;
132 
133 /* scale to fit or something more complex? */
134 	float ar = (float)src->w / (float)src->h;
135 	if (state->out_w && state->out_h &&
136 		state->out_w <= dw && state->out_h <= dh){
137 		dw = state->out_w;
138 		dh = state->out_h;
139 	}
140 
141 /* reduce to fit aspect? */
142 	float new_ar = (float)dw / (float)dh;
143 	if (state->aspect_ratio && fabs(new_ar - ar) > AR_EPSILON){
144 /* bias against the dominant axis */
145 		debug_message("blit[adjust %f] %d*%d -> ", ar, dw, dh);
146 		float wr = src->w / dst->w;
147 		float hr = src->h / dst->h;
148 		dw = hr > wr ? dst->h * ar : dst->w;
149 		dh = hr < wr ? dst->w / ar : dst->h;
150 		debug_message("%d*%d\n", dw, dh);
151 	}
152 
153 /* sanity check */
154 	if (src->buf_sz != src->w * src->h * 4)
155 		return;
156 
157 	int pad_w = dst->w - dw;
158 	int pad_h = dst->h - dh;
159 	int src_stride = src->w * 4;
160 
161 /* early out, no transform */
162 	if (dw == dst->w && dh == dst->h &&
163 		src->w == dst->w && src->h == dst->h){
164 		debug_message("full-blit[%d*%d]\n", (int)dst->w, (int)dst->h);
165 		for (size_t row = 0; row < dst->h; row++)
166 			memcpy(&dst->vidp[row*dst->pitch],
167 				&src->buf[row*src_stride], src_stride);
168 		goto done;
169 	}
170 
171 	int pad_pre_x = pad_w >> 1;
172 	int pad_pre_y = pad_h >> 1;
173 
174 /* FIXME: stretch-blit/transform for zoom in/out or pan */
175 	debug_message("blit[%d+%d*%d+%d] -> [%d,%d]:pad(%d,%d)\n",
176 		(int)src->w, (int)src->x, (int)src->h, (int)src->y,
177 		(int)dw, (int)dh, pad_w, pad_h);
178 	stbir_resize_uint8(
179 		&src->buf[src->y * src_stride + src->x],
180 		src->w - src->x, src->h - src->y,
181 		src_stride,
182 		(uint8_t*) &dst->vidp[pad_pre_y * dst->pitch + pad_pre_x],
183 		dw, dh, dst->stride, sizeof(shmif_pixel)
184 	);
185 
186 /* pad beginning / end rows */
187 	for (int y = 0; y < pad_pre_y; y++){
188 		shmif_pixel* vidp = dst->vidp + y * dst->pitch;
189 		for (int x = 0; x < dst->w; x++)
190 			vidp[x] = state->pad_col;
191 	}
192 
193 	for (int y = pad_pre_y + dh; y < dst->h; y++){
194 		shmif_pixel* vidp = dst->vidp + y * dst->pitch;
195 		for (int x = 0; x < dst->w; x++)
196 			vidp[x] = state->pad_col;
197 	}
198 
199 /* pad with color */
200 	for (int y = pad_pre_y; y < pad_pre_y + dh; y++){
201 		shmif_pixel* vidp = dst->vidp + y * dst->pitch;
202 		for (int x = 0; x < pad_pre_x; x++)
203 			vidp[x] = state->pad_col;
204 
205 		for (int x = pad_pre_x + dw; x < dst->w; x++)
206 			vidp[x] = state->pad_col;
207 	}
208 
209 done:
210 	arcan_shmif_signal(dst, SHMIF_SIGVID | SHMIF_SIGBLK_NONE);
211 }
212 
set_ident(struct arcan_shmif_cont * out,const char * prefix,const char * str)213 static void set_ident(struct arcan_shmif_cont* out,
214 	const char* prefix, const char* str)
215 {
216 	struct arcan_event ev = {.ext.kind = ARCAN_EVENT(IDENT)};
217 	size_t lim = sizeof(ev.ext.message.data)/sizeof(ev.ext.message.data[1]);
218 	size_t len = strlen(str);
219 
220 /* strip away overflowing prefix, assume suffix is more important */
221 	if (len > lim)
222 		str += len - lim - 1;
223 
224 	snprintf((char*)ev.ext.message.data, lim, "%s%s", prefix, str);
225 	debug_message("new ident: %s%s\n", prefix, str);
226 	arcan_shmif_enqueue(out, &ev);
227 }
228 
229 /*
230  * last_ds state is needed as this is mapped into the STB- code that
231  * provides no other interface, just callback with value, no ctx.
232  */
progress_report(float state)233 static void progress_report(float state)
234 {
235 	static float last_state;
236 	if (state < 1.0){
237 		char msgbuf[16];
238 		if (state - last_state > 0.1){
239 			last_state = state;
240 			snprintf(msgbuf, 16, "scale(%.2d%%) ", (int)(state*100));
241 			set_ident(last_ds->con, msgbuf, last_ds->playlist[last_ds->pl_ind].fname);
242 		}
243 	}
244 	else{
245 		set_ident(last_ds->con, "", last_ds->playlist[last_ds->pl_ind].fname);
246 		last_state = 0.0;
247 	}
248 }
249 
update_item(struct draw_state * ds,struct img_state * i,int step)250 static bool update_item(struct draw_state* ds, struct img_state* i, int step)
251 {
252 	if (imgload_poll(i)){
253 		if (i->is_stdin)
254 			ds->stdin_pending = false;
255 
256 		i->life = 0;
257 		ds->wnd_pending--;
258 
259 /* OK poll result is just that it's finished, not that it succeeded */
260 		if (!i->broken){
261 			debug_message("worker (%s) finished\n", i->fname);
262 			ds->wnd_act++;
263 			return true;
264 		}
265 		else {
266 			debug_message("worker (%s) broken: %s\n", i->fname, i->msg);
267 			if (&ds->playlist[ds->pl_ind] == i){
268 /* item broken, just jump to next */
269 				if (!set_playlist_pos(ds, ds->pl_ind + 1))
270 					return false;
271 			}
272 		}
273 	}
274 	else if (step && i->life > 0){
275 //		i->life--;
276 		if (!i->life){
277 			debug_message("worker (%s) timed out\n", i->fname);
278 			imgload_reset(i);
279 			ds->wnd_pending--;
280 			i->life = -1;
281 		}
282 	}
283 	return false;
284 }
285 
286 /* sweep O(n) slots for pending loads as we want to reel them in immediately
287  * to keep the active number of zombies down and shrink our own memory alloc */
poll_pl(struct draw_state * ds,int step)288 static bool poll_pl(struct draw_state* ds, int step)
289 {
290 	bool update = false;
291 	for (size_t i = 0; i < ds->pl_size && ds->wnd_pending; i++){
292 		struct img_state* is = &ds->playlist[i];
293 		if (is->proc)
294 			update |= update_item(ds, is, step);
295 	}
296 
297 /* special treatment for the currently selected index, have a retry timer
298  * on failure, update ident with current load status */
299 	struct img_state* cur = &ds->playlist[ds->pl_ind];
300 	if (!ds->loaded && (cur->broken || (cur->out && cur->out->ready))){
301 		set_ident(ds->con, (char*) cur->msg, cur->fname);
302 		ds->loaded = update = true;
303 	}
304 	else {
305 /* progress ident is updated in callback, scheduling is done in set_pl_pos,
306  * but we can set the steam status to match our playlist position vs. playlist
307  * size */
308 	}
309 
310 	return update;
311 }
312 
313 /* used to spawn a new worker process */
try_dispatch(struct draw_state * ds,int ind,int prio_d)314 static bool try_dispatch(struct draw_state* ds, int ind, int prio_d)
315 {
316 /* already loaded and acknowledged? */
317 	if (ds->playlist[ind].out && ds->playlist[ind].life >= 0)
318 		return false;
319 
320 /* or stdin- slot one is already being loaded from standard input */
321 	if (ds->playlist[ind].is_stdin && ds->stdin_pending)
322 		return false;
323 
324 /* NOTE: we don't care if the entry has previously been marked as broken,
325  * the specified input may have appeared or permissions might have changed */
326 
327 /* all done, spawn */
328 	if (imgload_spawn(ds->con, &ds->playlist[ind], prio_d)){
329 		if (ds->playlist[ind].is_stdin)
330 			ds->stdin_pending = true;
331 		ds->wnd_pending++;
332 		ds->playlist[ind].life = ds->timeout;
333 			debug_message("queued %s[%d], pending: %d\n",
334 			ds->playlist[ind].fname,  ind, ds->wnd_pending);
335 		return true;
336 	}
337 	else
338 		debug_message("imgload_spawn failed on [%d]\n", ind);
339 
340 	return false;
341 }
342 
reset_slot(struct draw_state * ds,int ind)343 static void reset_slot(struct draw_state* ds, int ind)
344 {
345 	if (ind >= 0 && ind < ds->pl_size && ds->playlist[ind].out){
346 		if (ds->playlist[ind].proc)
347 			ds->wnd_pending--;
348 		else if (!ds->playlist[ind].broken)
349 			ds->wnd_act--;
350 		imgload_reset(&ds->playlist[ind]);
351 		ds->playlist[ind].life = -1;
352 	}
353 }
354 
clean_queue(struct draw_state * ds)355 static void clean_queue(struct draw_state* ds)
356 {
357 /* scan horizon to determine the number of entries to drop, assumptions from
358  * caller is that we're in a state where entries actually need to be dropped */
359 	size_t fwd = ds->pl_ind;
360 	size_t ntr = ds->wnd_fwd;
361 
362 /* A special case to consider: when we step backwards in playlist and exceeds
363  * the window, that'll cause the window to grow outside bounds.  This is
364  * accepted (for now) on the motivation that the forward direction is more
365  * important and is the expected use-case. To change this, simply take
366  * step- direction into account. */
367 	while (ntr){
368 		if (!ds->playlist[fwd].out)
369 			if (!ds->playlist[fwd].broken)
370 				break;
371 		ntr--;
372 		fwd = fwd + 1;
373 		if (fwd == ds->pl_size){
374 			if (ds->loop)
375 				fwd = 0;
376 			else {
377 				ntr = 0;
378 			}
379 		}
380 	}
381 	debug_message("clean_queue(%zu)\n", ntr);
382 
383 	if (!ntr)
384 		return;
385 
386 /* find oldest item */
387 	size_t back = ds->pl_ind > 0 ? ds->pl_ind - 1 : ds->pl_size - 1;
388 	while (ds->playlist[back].out || ds->playlist[back].broken)
389 		back = back > 0 ? back - 1 : ds->pl_size - 1;
390 
391 /* and start the purge */
392 	while (ntr){
393 		if (ds->playlist[back].out){
394 			debug_message("unloading tail@%zu (%s), left: %zu\n",
395 				back, ds->playlist[back].fname, ntr);
396 			reset_slot(ds, back);
397 			ntr--;
398 		}
399 		back = (back + 1) % ds->pl_size;
400 	}
401 }
402 
set_playlist_pos(struct draw_state * ds,int new_i)403 static bool set_playlist_pos(struct draw_state* ds, int new_i)
404 {
405 	size_t pos;
406 
407 /* range-check, if we don't loop, return to start */
408 	if (new_i < 0)
409 		new_i = ds->pl_size-1;
410 
411 	if (new_i >= ds->pl_size){
412 		if (ds->loop){
413 			debug_message("looping");
414 			new_i = 0;
415 		}
416 		else{
417 			debug_message("playlist out of bounds, shut down\n");
418 			arcan_shmif_drop(ds->con);
419 			return false;
420 		}
421 	}
422 
423 /* if we land outside the previous window, full-reset everything */
424 	if (ds->wnd_lim && abs(new_i - ds->pl_ind) > ds->wnd_lim){
425 		for (size_t i = 0; i < ds->pl_size; i++)
426 			reset_slot(ds, i);
427 		ds->wnd_act = 0;
428 		ds->wnd_pending = 0;
429 	}
430 
431 /* first dispatch the currently requested slot at normal priority */
432 	ds->pl_ind = new_i;
433 	struct img_state* cur = &ds->playlist[ds->pl_ind];
434 	if (!cur->out){
435 		debug_message("single load (%s)\n", cur->fname);
436 		try_dispatch(ds, ds->pl_ind, 0);
437 	}
438 
439 /* if we overshoot or are at capacity, start flushing out */
440 	if (ds->wnd_lim && ds->pl_size > ds->wnd_lim &&
441 		ds->wnd_pending + ds->wnd_act >= ds->wnd_lim){
442 		clean_queue(ds);
443 	}
444 
445 /* and then fill up with lesser priority, note that there is a slight degrade
446  * if we step from the currently loaded into one that is pending as the
447  * priority for that one, though the effect should be minimal enough to not
448  * bothering with renicing */
449 	pos = (new_i + 1) % ds->pl_size;
450 
451 	while (pos != new_i &&
452 		(ds->wnd_act + ds->wnd_pending < ds->wnd_lim || !ds->wnd_lim)){
453 		debug_message("attempt to queue (%s)\n", ds->playlist[pos].fname);
454 		try_dispatch(ds, pos, 1);
455 		pos = (pos + 1) % ds->pl_size;
456 	}
457 
458 	debug_message("position set to (%d, act: %d/%d, pending: %d)\n",
459 		ds->pl_ind, ds->wnd_act, ds->wnd_lim, ds->wnd_pending);
460 
461 	arcan_shmif_enqueue(ds->con, &(struct arcan_event){
462 		.category = EVENT_EXTERNAL,
463 		.ext.kind = ARCAN_EVENT(STREAMSTATUS),
464 		.ext.streamstat.completion = (float)ds->pl_ind / (float)ds->pl_size,
465 		.ext.streamstat.frameno = ds->pl_ind
466 	});
467 
468 	for (size_t i = 0; i < ds->pl_size; i++)
469 		debug_message("%c ", i == ds->pl_ind ? '*' :
470 			(ds->playlist[i].out ? 'o' : 'x'));
471 	debug_message("\n");
472 
473 	return true;
474 }
475 
476 struct lent {
477 	const char* lbl;
478 	const char* descr;
479 	int defsym;
480 	bool(*ptr)(struct draw_state*);
481 };
482 
step_next(struct draw_state * state)483 static bool step_next(struct draw_state* state)
484 {
485 	set_playlist_pos(state, state->pl_ind + 1);
486 	return true;
487 }
488 
step_prev(struct draw_state * state)489 static bool step_prev(struct draw_state* state)
490 {
491 	set_playlist_pos(state, state->pl_ind - 1);
492 	return true;
493 }
494 
source_size(struct draw_state * state)495 static bool source_size(struct draw_state* state)
496 {
497 	if (!state->source_size){
498 		state->source_size = true;
499 		state->blit_ind = -1;
500 	}
501 	return false;
502 }
503 
server_size(struct draw_state * state)504 static bool server_size(struct draw_state* state)
505 {
506 	if (state->source_size){
507 		state->source_size = false;
508 		state->blit_ind = -1;
509 		return arcan_shmif_resize(state->con, state->out_w, state->out_h);
510 	}
511 	else
512 		return false;
513 }
514 
pl_toggle(struct draw_state * state)515 static bool pl_toggle(struct draw_state* state)
516 {
517 	state->step_block = !state->step_block;
518 	return false;
519 }
520 
aspect_ratio(struct draw_state * state)521 static bool aspect_ratio(struct draw_state* state)
522 {
523 	state->aspect_ratio = !state->aspect_ratio;
524 	return true;
525 }
526 
527 static const struct lent labels[] = {
528 	{"PREV", "Step to previous entry in playlist", TUIK_H, step_prev},
529 	{"NEXT", "Step to next entry in playlist", TUIK_L, step_next},
530 	{"PL_TOGGLE", "Toggle playlist stepping on/off", TUIK_SPACE, pl_toggle},
531 	{"SOURCE_SIZE", "Resize the window to fit image size", TUIK_F5, source_size},
532 	{"SERVER_SIZE", "Use the recommended connection size", TUIK_F6, server_size},
533 	{"ASPECT_TOGGLE", "Maintain aspect ratio", TUIK_TAB, aspect_ratio},
534 /*
535  * "ZOOM_IN", "Increment the scale factor (integer)", "F1", TUIK_F1, zoom_in},
536 	{"ZOOM_OUT", "Decrement the scale factor (integer)", "F2", TUIK_F2, zoom_out},
537  */
538 	{NULL, NULL}
539 };
540 
find_label(const char * label)541 static const struct lent* find_label(const char* label)
542 {
543 	const struct lent* cur = labels;
544 	while(cur->ptr){
545 		if (strcmp(label, cur->lbl) == 0)
546 			return cur;
547 		cur++;
548 	}
549 	return NULL;
550 }
551 
find_sym(unsigned sym)552 static const struct lent* find_sym(unsigned sym)
553 {
554 	const struct lent* cur = labels;
555 	if (sym == 0)
556 		return NULL;
557 
558 	while(cur->ptr){
559 		if (cur->defsym == sym)
560 			return cur;
561 		cur++;
562 	}
563 
564 	return NULL;
565 }
566 
dispatch_event(struct arcan_shmif_cont * con,struct arcan_event * ev,struct draw_state * ds)567 static bool dispatch_event(
568 	struct arcan_shmif_cont* con, struct arcan_event* ev, struct draw_state* ds)
569 {
570 	if (ev->category == EVENT_IO){
571 		if (ds->block_input)
572 			return false;
573 
574 /* drag/zoom/pan/... */
575 		if (ev->io.devkind == EVENT_IDEVKIND_MOUSE){
576 			return false;
577 		}
578 		else if (ev->io.datatype == EVENT_IDATATYPE_DIGITAL){
579 			if (!ev->io.input.translated.active || !ev->io.label[0])
580 				return false;
581 
582 			const struct lent* lent = find_label(ev->io.label);
583 			if (lent){
584 				if (lent->ptr(ds)){
585 					debug_message("dirty from [digital] input\n");
586 					return true;
587 				}
588 			}
589 		}
590 		else if (ev->io.datatype == EVENT_IDATATYPE_TRANSLATED){
591 			if (!ev->io.input.translated.active)
592 				return false;
593 			const struct lent* lent = ev->io.label[0] ?
594 				find_label(ev->io.label) : find_sym(ev->io.input.translated.keysym);
595 			if (lent && lent->ptr(ds)){
596 				debug_message("dirty from [translated] input\n");
597 				return true;
598 			}
599 		}
600 	}
601 	else if (ev->category == EVENT_TARGET)
602 		switch(ev->tgt.kind){
603 		case TARGET_COMMAND_DISPLAYHINT:
604 			if (ev->tgt.ioevs[0].iv && ev->tgt.ioevs[1].iv &&
605 				(ev->tgt.ioevs[0].iv != con->w || ev->tgt.ioevs[1].iv != con->h)){
606 				if (!ds->source_size && arcan_shmif_resize(
607 					con, ev->tgt.ioevs[0].iv, ev->tgt.ioevs[1].iv)){
608 						debug_message("server requested [%d*%d] vs. current [%d*%d]\n",
609 							ev->tgt.ioevs[0].iv, ev->tgt.ioevs[1].iv, con->w, con->h);
610 						return true;
611 				}
612 			}
613 		break;
614 		case TARGET_COMMAND_STEPFRAME:
615 			if (ev->tgt.ioevs[1].iv == 0xfeed)
616 				poll_pl(ds, 1);
617 
618 			if (ds->step_timer > 0 && !ds->step_block){
619 				if (ds->step_timer == 0){
620 					ds->step_timer = ds->init_timer;
621 					set_playlist_pos(ds, ds->pl_ind + 1);
622 					return true;
623 				}
624 			}
625 		break;
626 		case TARGET_COMMAND_EXIT:
627 			arcan_shmif_drop(con);
628 			return false;
629 		break;
630 		default:
631 		break;
632 		}
633 	return false;
634 }
635 
show_use(const char * msg)636 static int show_use(const char* msg)
637 {
638 	printf("Usage: aloadimage [options] file1 .. filen\n"
639 "Basic Use:\n"
640 "-h     \t--help        \tThis text\n"
641 "-a     \t--aspect      \tMaintain aspect ratio when scaling\n"
642 "-S     \t--server-size \tScale to fit server- suggested window size\n"
643 "-l     \t--loop        \tStep back to [file1] after reaching [filen] in playlist\n"
644 "-t sec \t--step-time   \tSet playlist step time (~seconds)\n"
645 "-b     \t--block-input \tIgnore keyboard and mouse input\n"
646 "-d str \t--display     \tSet/override the display server connection path\n"
647 "-p rgba\t--padcol      \tSet the padding color, like -p 127,127,127,255\n"
648 "Tuning:\n"
649 "-m num \t--limit-mem   \tSet loader process memory limit to [num] MB\n"
650 "-r num \t--readahead   \tSet the playlist window queue size\n"
651 "-T sec \t--timeout     \tSet unresponsive worker kill- timeout\n"
652 "-H     \t--vr          \tSet stereoscopic mode, prefix files with l: or r:\n"
653 #ifdef ENABLE_SECCOMP
654 "-X    \t--no-sysflt   \tDisable seccomp- syscall filtering\n"
655 #endif
656 	);
657 	return EXIT_FAILURE;
658 }
659 
expose_labels(struct arcan_shmif_cont * con)660 static void expose_labels(struct arcan_shmif_cont* con)
661 {
662 	const struct lent* cur = labels;
663 	while(cur->lbl){
664 		arcan_event ev = {
665 			.category = EVENT_EXTERNAL,
666 			.ext.kind = ARCAN_EVENT(LABELHINT),
667 			.ext.labelhint.initial = cur->defsym,
668 			.ext.labelhint.idatatype = EVENT_IDATATYPE_DIGITAL
669 		};
670 			snprintf(ev.ext.labelhint.label,
671 			sizeof(ev.ext.labelhint.label)/sizeof(ev.ext.labelhint.label[0]),
672 			"%s", cur->lbl
673 		);
674 		cur++;
675 		arcan_shmif_enqueue(con, &ev);
676 	}
677 }
678 
679 static const struct option longopts[] = {
680 	{"help", no_argument, NULL, 'h'},
681 	{"loop", no_argument, NULL, 'l'},
682 	{"step-time", required_argument, NULL, 't'},
683 	{"block-input", no_argument, NULL, 'b'},
684 	{"timeout", required_argument, NULL, 'T'},
685 	{"limit-mem", required_argument, NULL, 'm'},
686 	{"readahead", required_argument, NULL, 'r'},
687 	{"padcol", required_argument, NULL, 'p'},
688 	{"no-sysflt", no_argument, NULL, 'X'},
689 	{"server-size", no_argument, NULL, 'S'},
690 	{"display", no_argument, NULL, 'd'},
691 	{"aspect", no_argument, NULL, 'a'},
692 	{"vr180", no_argument, NULL, 'H'},
693 	{ NULL, no_argument, NULL, 0 }
694 };
695 
696 /* wait but using the epipe and multiplex with current playlist item */
wait_for_event(struct draw_state * ds)697 static void wait_for_event(struct draw_state* ds)
698 {
699 	short pollev = POLLIN | POLLERR | POLLNVAL | POLLHUP;
700 	struct pollfd fds[2] = {
701 	{
702 		.events = pollev,
703 		.fd = ds->con->epipe
704 	},
705 	{
706 		.events = pollev,
707 		.fd = -1
708 	},
709 	};
710 	size_t pollsz = 1;
711 	int plfd = ds->playlist[ds->pl_ind].sigfd;
712 	if (plfd > 0){
713 		fds[1].fd = plfd;
714 		pollsz++;
715 	}
716 
717 /* don't care about the result, only used for sleep / wake */
718 	poll(fds, pollsz, -1);
719 }
720 
main(int argc,char ** argv)721 int main(int argc, char** argv)
722 {
723 	if (argc <= 1)
724 		return show_use("invalid/missing arguments");
725 
726 	struct img_state playlist[argc];
727 	struct draw_state ds = {
728 		.source_size = true,
729 		.wnd_lim = 5,
730 		.init_timer = 0,
731 		.pad_col = SHMIF_RGBA(32, 32, 32, 255),
732 		.playlist = playlist,
733 		.blit_ind = -1
734 	};
735 	last_ds = &ds;
736 
737 	int ch;
738 	int segid = SEGID_MEDIA;
739 
740 	while((ch = getopt_long(argc, argv,
741 		"p:ihlt:bd:T:m:r:XSHd:a", longopts, NULL)) >= 0)
742 		switch(ch){
743 		case 'h' : return show_use(""); break;
744 		case 't' : ds.init_timer = strtoul(optarg, NULL, 10) * 5; break;
745 		case 'b' : ds.block_input = true; break;
746 		case 'd' : setenv("ARCAN_CONNPATH", optarg, 1); break;
747 		case 'T' : ds.timeout = strtoul(optarg, NULL, 10) * 5; break;
748 		case 'l' : ds.loop = true; break;
749 		case 'f' : ds.wnd_fwd = strtoul(optarg, NULL, 10); break;
750 		case 'p' : {
751 			uint8_t outv[4] = {0, 0, 0, 255};
752 			if (sscanf(optarg, "%"SCNu8",%"SCNu8",%"SCNu8",%"SCNu8,
753 				&outv[0], &outv[1], &outv[2], &outv[3])){
754 					ds.pad_col = SHMIF_RGBA(outv[0], outv[1], outv[2], outv[3]);
755 			}
756 		} break;
757 		case 'a' : ds.aspect_ratio = true; break;
758 		case 'm' : image_size_limit_mb = strtoul(optarg, NULL, 10); break;
759 		case 'r' : ds.wnd_lim = strtoul(optarg, NULL, 10); break;
760 		case 'X' : disable_syscall_flt = true; break;
761 		case 'N' : ds.handover_exec = true; break;
762 		case 'H' :
763 			ds.stereo = true;
764 			segid = SEGID_HMD_L;
765 		break;
766 		case 'S' :
767 			ds.source_size = false;
768 			segid = SEGID_APPLICATION;
769 		break;
770 		default:
771 			fprintf(stderr, "unknown/ignored option: %c\n", ch);
772 		break;
773 		}
774 	ds.step_timer = ds.init_timer;
775 
776 /* there are more considerations here -
777  *
778  * some image formats will be packed l/r and in those cases we'd want to split (possibly use lr:)
779  * along the dominant axis.
780  *
781  * other image formats are actually n images in one packed, something we don't support right now,
782  * there also needs to be some way to specify the server-side projection geometry
783  */
784 	if (ds.stereo){
785 		int rc = 0, lc = 0;
786 		bool last_l = false;
787 
788 		for (int i = optind; i < argc; i++){
789 			if ((argv[i][0] != 'l' && argv[i][0] != 'r') || argv[i][1] != ':'){
790 				fprintf(stderr, "malformed entry (%d): %s\n"
791 					"vr mode (-H,--vr) requires l: and r: prefix for each entry\n", i - optind + 1, argv[i]);
792 				return EXIT_FAILURE;
793 			}
794 
795 			if (argv[i][0] == 'l'){
796 				if (last_l){
797 					fprintf(stderr, "malformed l-entry (%d): %s\n"
798 						"vr mode (-H,--vr) requires l: and r: to interleave\n", i - optind + 1, &argv[i][2]);
799 				}
800 				last_l = true;
801 				lc++;
802 			}
803 			else{
804 				if (!last_l){
805 					fprintf(stderr, "malformed r-entry (%d): %s\n"
806 						"vr mode (-H,--vr) requires l: and r: to interleave\n", i - optind + 1, &argv[i][2]);
807 					return EXIT_FAILURE;
808 				}
809 				last_l = false;
810 				rc++;
811 			}
812 
813 			playlist[ds.pl_size++] = (struct img_state){
814 				.fd = -1,
815 				.stereo_right = argv[i][0] == 'r',
816 				.fname = &argv[i][2],
817 				.is_stdin = false
818 			};
819 		}
820 		if (rc != lc){
821 			fprintf(stderr, "malformed number of arguments (l=%d, r=%d)\n"
822 				"vr mode (-X,--vr) requires an even number of l: and r: prefixed entries\n", lc, rc);
823 			return EXIT_FAILURE;
824 		}
825 	}
826 	else {
827 		for (int i = optind; i < argc; i++){
828 			playlist[ds.pl_size] = (struct img_state){
829 				.fd = -1,
830 				.fname = argv[i],
831 				.is_stdin = strcmp(argv[i], "-") == 0
832 			};
833 			ds.pl_size++;
834 		}
835 	}
836 
837 /* sanity- clamp, always preload something*/
838 	if (ds.wnd_lim)
839 		ds.wnd_fwd = ds.wnd_lim > 2 ? ds.wnd_lim - 2 : 2;
840 
841 	struct arcan_shmif_cont cont = arcan_shmif_open_ext(
842 		SHMIF_ACQUIRE_FATALFAIL, NULL, (struct shmif_open_ext){
843 			.type = segid,
844 			.title = "aloadimage",
845 			.ident = ""
846 		}, sizeof(struct shmif_open_ext)
847 	);
848 
849 	if (ds.pl_size == 0){
850 		arcan_shmif_last_words(&cont, "empty playlist");
851 		arcan_shmif_drop(&cont);
852 		return EXIT_FAILURE;
853 	}
854 
855 	struct arcan_shmif_initial* init;
856 	arcan_shmif_initial(&cont, &init);
857 	if (init && init->density > 0){
858 		ds.dpi = init->density / 2.5;
859 	}
860 
861 	ds.con = &cont;
862 
863 /* request the right segment, deal with it if it arrives */
864  if (ds.stereo){
865 	 arcan_shmif_enqueue(&cont, &(struct arcan_event){
866 			.ext.kind = ARCAN_EVENT(SEGREQ),
867 			.ext.segreq.kind = SEGID_HMD_R,
868 			.ext.segreq.id = 0x6502
869 		});
870  }
871 
872 /* dispatch workers */
873 	set_playlist_pos(&ds, 0);
874 
875 /* 200ms timer for automatic stepping and load/poll, if this value is
876  * changed, do trhe same for the multipliers to init_timer and timeout */
877 	arcan_shmif_enqueue(&cont, &(struct arcan_event){
878 		.ext.kind = ARCAN_EVENT(CLOCKREQ),
879 		.ext.clock.rate = 5,
880 		.ext.clock.id = 0xfeed
881 	});
882 
883 /* Block for events - timer should wake us up often enough, otherwise we need
884  * a signal descriptor from the playlist processing and multiplex on both evdesc
885  * and on the playlist */
886 	struct arcan_event ev;
887 	while (cont.addr && (wait_for_event(&ds), 1)){
888 		int pv;
889 		int dirty = 0;
890 
891 		while((pv = arcan_shmif_poll(&cont, &ev)) > 0)
892 			dirty |= dispatch_event(&cont, &ev, &ds);
893 
894 		if (pv == -1)
895 			break;
896 
897 /* If we are waiting for the currently selected item to finish processing */
898 		dirty |= poll_pl(&ds, 0);
899 
900 /* blit and update title as playlist position might have changed, or some other
901  * metadata we present as part of the ident/title - if we are in stereo mode we
902  * simply skip ahead one (l/r interleaved) and then send the second to the
903  * other context, can do on a secondary thread */
904 		struct img_state* cur = &ds.playlist[ds.pl_ind];
905 
906 		if (dirty && !cur->broken && cur->out && cur->out->ready){
907 			set_ident(ds.con, "", cur->fname);
908 
909 			if (ds.pl_ind != ds.blit_ind){
910 				ds.blit_ind = ds.pl_ind;
911 				blit(&cont, (struct img_data*) cur->out, &ds);
912 			}
913 		}
914 	}
915 
916 /* reset the entire playlist so leak detection is useful */
917 	for (size_t i = 0; i < ds.pl_size; i++)
918 		imgload_reset(&ds.playlist[i]);
919 
920 	arcan_shmif_drop(&cont);
921 	if (ds.con_right)
922 		arcan_shmif_drop(ds.con_right);
923 
924 	return EXIT_SUCCESS;
925 }
926