Mir

Merge lp:~vanvugt/mir/target into lp:mir

Proposed by Daniel van Vugt
Status: Merged
Approved by: Daniel van Vugt
Approved revision: no longer in the source branch.
Merged at revision: 2464
Proposed branch: lp:~vanvugt/mir/target
Merge into: lp:mir
Prerequisite: lp:~mir-team/mir/fix-1438160
Diff against target: 363 lines (+348/-0)
2 files modified
examples/CMakeLists.txt (+6/-0)
examples/target.c (+342/-0)
To merge this branch: bzr merge lp:~vanvugt/mir/target
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Robert Carr (community) Approve
Alan Griffiths Approve
Review via email: mp+254885@code.launchpad.net

Commit message

Introducing mir_demo_client_target, for visual testing of input latency.

This provides a visual scale by which to grade input latency, particularly
when watching the mouse pointer. Such a small scale is required if you are
to reliably see any improvement (or regression) of only a few milliseconds
latency.

mir_demo_client_target also does its own input sampling in place of
Mir's default resampling, so achieves about half a frame lower latency on
average. Even with the default swap interval of one. Although forcing swap
interval to zero (-n option) reduces visible latency even more. Doing its
own sampling also allows mir_demo_client_target to render at the full
native frame rate, rather than being limited by the Mir input resampling
rate.

Tip: If you find it hard to see the cursor against the target then using
the zoom feature (Super+mousewheel) of mir_proving_server helps. Because
that will visually amplify any offset.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

The failure seems to be unrelated...

28: C++ exception with description "stop_server() failed to stop server" thrown in TearDown().
Build timed out (after 120 minutes). Marking the build as failed.
Build was aborted
Build was marked for publishing on https://jenkins.qa.ubuntu.com/
Finished: FAILURE

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

Definitely not related to this proposal...

10: [ RUN ] ClientLibrary.create_simple_normal_surface_from_spec
10: [1427959486.308220] Server: Starting
10: [1427959486.308335] Platform Loader: Selected driver: dummy (version 0.13.0)
10: [1427959486.309135] mirserver: Using software cursor
10: [1427959486.311136] DisplayServer: Mir version 0.13.0
10: /mir/tests/acceptance-tests/test_client_library.cpp:655: Failure
10: Value of: native_buffer->width
10: Expected: is equal to 800
10: Actual: 1 (of type int)
10: /mir/tests/acceptance-tests/test_client_library.cpp:656: Failure
10: Value of: native_buffer->height
10: Expected: is equal to 600
10: Actual: 1 (of type int)
10: [1427959486.318845] Server: Stopping
10: [ FAILED ] ClientLibrary.create_simple_normal_surface_from_spec (13 ms)

Revision history for this message
Robert Carr (robertcarr) wrote :

I see no problem with this landing but I'm not sure about the analysis/goal.

>> mir_demo_client_target also does its own input resampling

It seems mir_demo_client_target is just sampling input rather than 'resampling'.

>> in place of
>> Mir's default resampling, so achieves about half a frame lower latency on
>> average.

From my understanding this would be the intentional re sampling latency...we can remove it at the cost of varying the sample rate per frame (which in the case of uniform speed scrolls results in jerkiness).

There are some useful visual depictions of what is going on here at:
http://www.masonchang.com/blog/2014/8/11/smooth-scrolling-frame-uniformity-touch-interpolation-and-touch-responsiveness

Revision history for this message
Daniel van Vugt (vanvugt) wrote :

You're right, this is just "sampling" input in the most efficient way. Not "re-sampling". I'll adjust the wording...

Indeed, not all apps can work this way. Those that have not been written to handle input efficiently will need the built-in input resampling. Also those that benefit from touch motion prediction will want that. But in theory any app/toolkit could be given optimal input handling by:

  1. Only wake up on input events.
  2. When woken, schedule a frame if none scheduled already.
  3. At the start of each frame, /get/ the latest input state (i.e. current cursor position).
  4. Use the latest input state to render your frame.

This is all stuff I remember being taught in 1998. It's kind of embarrassing we forget it so often.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Alan Griffiths (alan-griffiths) wrote :

Looks like the 60's are calling but OK

review: Approve
Revision history for this message
Robert Carr (robertcarr) wrote :

>> But in theory any app/toolkit could be given optimal input handling by
>> 1. Only wake up on input events.
>> 2. When woken, schedule a frame if none scheduled already.
>> 3. At the start of each frame, /get/ the latest input state (i.e. current cursor position).
>> 4. Use the latest input state to render your frame.

This seems like it would be true but I don't think it is. What this example fails to take in to account is that the "latest input state" is updated at a varying rate from the frame.

This article:
http://www.masonchang.com/blog/2014/8/11/smooth-scrolling-frame-uniformity-touch-interpolation-and-touch-responsiveness

Has a good visual depiction of unresampled input with a sample rate mismatch (the "current code" version of firefox logo).

Example is ok though

review: Approve
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

Absolutely in touch scrolling uniformity needs to be considered. That's why we still have a fixed input resampling interval and it still looks better than our attempts to adjust it dynamically did.

But this demo is mainly for tracking the cursor as quickly as possible. Not animation.

Revision history for this message
PS Jenkins bot (ps-jenkins) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'examples/CMakeLists.txt'
2--- examples/CMakeLists.txt 2015-03-31 12:48:35 +0000
3+++ examples/CMakeLists.txt 2015-04-07 07:01:52 +0000
4@@ -50,6 +50,12 @@
5 target_link_libraries(mir_demo_client_egltriangle
6 eglapp
7 )
8+mir_add_wrapped_executable(mir_demo_client_target
9+ target.c
10+)
11+target_link_libraries(mir_demo_client_target
12+ eglapp
13+)
14 mir_add_wrapped_executable(mir_demo_client_eglcounter
15 eglcounter.cpp
16 )
17
18=== added file 'examples/target.c'
19--- examples/target.c 1970-01-01 00:00:00 +0000
20+++ examples/target.c 2015-04-07 07:01:52 +0000
21@@ -0,0 +1,342 @@
22+/*
23+ * Copyright © 2015 Canonical Ltd.
24+ *
25+ * This program is free software: you can redistribute it and/or modify
26+ * it under the terms of the GNU General Public License version 3 as
27+ * published by the Free Software Foundation.
28+ *
29+ * This program is distributed in the hope that it will be useful,
30+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
31+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32+ * GNU General Public License for more details.
33+ *
34+ * You should have received a copy of the GNU General Public License
35+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
36+ *
37+ * Author: Daniel van Vugt <daniel.van.vugt@canonical.com>
38+ */
39+
40+#define _POSIX_C_SOURCE 200112L // for setenv() from stdlib.h
41+#include "eglapp.h"
42+#include <assert.h>
43+#include <stdio.h>
44+#include <math.h>
45+#include <GLES2/gl2.h>
46+#include <mir_toolkit/mir_surface.h>
47+#include <pthread.h>
48+#include <stdlib.h>
49+#include <signal.h>
50+
51+enum
52+{
53+ max_touches = 10
54+};
55+
56+typedef struct
57+{
58+ float x, y;
59+} Vec2;
60+
61+typedef struct
62+{
63+ int points;
64+ Vec2 point[max_touches];
65+} TouchState;
66+
67+typedef struct
68+{
69+ pthread_mutex_t mutex;
70+ pthread_cond_t change;
71+ bool changed;
72+
73+ bool resized;
74+ TouchState touch;
75+} State;
76+
77+static GLuint load_shader(const char *src, GLenum type)
78+{
79+ GLuint shader = glCreateShader(type);
80+ if (shader)
81+ {
82+ GLint compiled;
83+ glShaderSource(shader, 1, &src, NULL);
84+ glCompileShader(shader);
85+ glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
86+ if (!compiled)
87+ {
88+ GLchar log[1024];
89+ glGetShaderInfoLog(shader, sizeof log - 1, NULL, log);
90+ log[sizeof log - 1] = '\0';
91+ printf("load_shader compile failed: %s\n", log);
92+ glDeleteShader(shader);
93+ shader = 0;
94+ }
95+ }
96+ return shader;
97+}
98+
99+GLuint generate_target_texture()
100+{
101+ const int width = 512, height = width;
102+ typedef struct { GLubyte r, b, g, a; } Texel;
103+ Texel image[height][width];
104+ // Note the 0.5f to convert from pixel corner (GL) to middle (image)
105+ const float centrex = width/2 - 0.5f, centrey = height/2 - 0.5f;
106+ const Texel blank = {0, 0, 0, 0};
107+ const int radius = centrex - 1;
108+ const Texel ring[] =
109+ {
110+ { 0, 0, 0, 255},
111+ { 0, 0, 255, 255},
112+ { 0, 255, 0, 255},
113+ {255, 255, 0, 255},
114+ {255, 128, 0, 255},
115+ {255, 0, 0, 255},
116+ };
117+ const int rings = sizeof(ring) / sizeof(ring[0]);
118+
119+ for (int y = 0; y < height; ++y)
120+ {
121+ for (int x = 0; x < width; ++x)
122+ {
123+ float dx = x - centrex, dy = y - centrey;
124+ int layer = rings * sqrtf(dx * dx + dy * dy) / radius;
125+ image[y][x] = layer < rings ? ring[layer] : blank;
126+ }
127+ }
128+
129+ GLuint tex;
130+ glGenTextures(1, &tex);
131+ glBindTexture(GL_TEXTURE_2D, tex);
132+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
133+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
134+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
135+ GL_LINEAR_MIPMAP_LINEAR);
136+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
137+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
138+ GL_UNSIGNED_BYTE, image);
139+ glGenerateMipmap(GL_TEXTURE_2D);
140+
141+ return tex;
142+}
143+
144+static void get_all_touch_points(const MirInputEvent *ievent, TouchState *touch)
145+{
146+ if (mir_input_event_get_type(ievent) == mir_input_event_type_pointer)
147+ {
148+ touch->points = 0;
149+ const MirPointerEvent *pevent =
150+ mir_input_event_get_pointer_event(ievent);
151+ if (mir_pointer_event_action(pevent) != mir_pointer_action_leave)
152+ {
153+ touch->points = 1;
154+ touch->point[0] = (Vec2)
155+ {
156+ mir_pointer_event_axis_value(pevent, mir_pointer_axis_x),
157+ mir_pointer_event_axis_value(pevent, mir_pointer_axis_y)
158+ };
159+ }
160+ }
161+ else if (mir_input_event_get_type(ievent) == mir_input_event_type_touch)
162+ {
163+ const MirTouchEvent *tevent = mir_input_event_get_touch_event(ievent);
164+ int n = mir_touch_event_point_count(tevent);
165+ if (n > max_touches)
166+ n = max_touches;
167+ bool all_up = true;
168+ for (int p = 0; p < n; ++p)
169+ {
170+ if (mir_touch_event_action(tevent, p) != mir_touch_action_up)
171+ all_up = false;
172+ touch->point[p] = (Vec2)
173+ {
174+ mir_touch_event_axis_value(tevent, p, mir_touch_axis_x),
175+ mir_touch_event_axis_value(tevent, p, mir_touch_axis_y)
176+ };
177+ }
178+ touch->points = all_up ? 0 : n;
179+ }
180+}
181+
182+static void on_event(MirSurface *surface, const MirEvent *event, void *context)
183+{
184+ (void)surface;
185+ State *state = (State*)context;
186+
187+ // FIXME: We presently need to know that events come in on a different
188+ // thread to main (LP: #1194384). When that's resolved, simple
189+ // single-threaded apps like this won't need pthread.
190+ pthread_mutex_lock(&state->mutex);
191+
192+ switch (mir_event_get_type(event))
193+ {
194+ case mir_event_type_input:
195+ get_all_touch_points(mir_event_get_input_event(event), &state->touch);
196+ break;
197+ case mir_event_type_resize:
198+ state->resized = true;
199+ break;
200+ case mir_event_type_close_surface:
201+ // TODO: eglapp.h needs a quit() function or different behaviour of
202+ // mir_eglapp_shutdown().
203+ raise(SIGTERM); // handled by eglapp
204+ break;
205+ default:
206+ break;
207+ }
208+
209+ state->changed = true;
210+ pthread_cond_signal(&state->change);
211+ pthread_mutex_unlock(&state->mutex);
212+}
213+
214+int main(int argc, char *argv[])
215+{
216+ const char vshadersrc[] =
217+ "attribute vec2 position;\n"
218+ "attribute vec2 texcoord;\n"
219+ "uniform float scale;\n"
220+ "uniform vec2 translate;\n"
221+ "uniform mat4 projection;\n"
222+ "varying vec2 v_texcoord;\n"
223+ "\n"
224+ "void main()\n"
225+ "{\n"
226+ " gl_Position = projection *\n"
227+ " vec4(position * scale + translate, 0.0, 1.0);\n"
228+ " v_texcoord = texcoord;\n"
229+ "}\n";
230+
231+ const char fshadersrc[] =
232+ "precision mediump float;\n"
233+ "varying vec2 v_texcoord;\n"
234+ "uniform sampler2D texture;\n"
235+ "\n"
236+ "void main()\n"
237+ "{\n"
238+ " gl_FragColor = texture2D(texture, v_texcoord);\n"
239+ "}\n";
240+
241+ // Disable Mir's input resampling. We do our own here, in a way that
242+ // has even lower latency than Mir's default algorithm.
243+ // TODO: Make a proper client API function for this:
244+ setenv("MIR_CLIENT_INPUT_RATE", "0", 0);
245+
246+ static unsigned int width = 0, height = 0;
247+ if (!mir_eglapp_init(argc, argv, &width, &height))
248+ return 1;
249+
250+ GLuint vshader = load_shader(vshadersrc, GL_VERTEX_SHADER);
251+ assert(vshader);
252+ GLuint fshader = load_shader(fshadersrc, GL_FRAGMENT_SHADER);
253+ assert(fshader);
254+ GLuint prog = glCreateProgram();
255+ assert(prog);
256+ glAttachShader(prog, vshader);
257+ glAttachShader(prog, fshader);
258+ glLinkProgram(prog);
259+
260+ GLint linked;
261+ glGetProgramiv(prog, GL_LINK_STATUS, &linked);
262+ if (!linked)
263+ {
264+ GLchar log[1024];
265+ glGetProgramInfoLog(prog, sizeof log - 1, NULL, log);
266+ log[sizeof log - 1] = '\0';
267+ printf("Link failed: %s\n", log);
268+ return 2;
269+ }
270+
271+ glUseProgram(prog);
272+
273+ const GLfloat square[] =
274+ { // position texcoord
275+ -0.5f, +0.5f, 0.0f, 1.0f,
276+ +0.5f, +0.5f, 1.0f, 1.0f,
277+ +0.5f, -0.5f, 1.0f, 0.0f,
278+ -0.5f, -0.5f, 0.0f, 0.0f,
279+ };
280+ GLint position = glGetAttribLocation(prog, "position");
281+ GLint texcoord = glGetAttribLocation(prog, "texcoord");
282+ glVertexAttribPointer(position, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat),
283+ square);
284+ glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat),
285+ square+2);
286+ glEnableVertexAttribArray(position);
287+ glEnableVertexAttribArray(texcoord);
288+
289+ GLint scale = glGetUniformLocation(prog, "scale");
290+ glUniform1f(scale, 128.0f);
291+
292+ GLint translate = glGetUniformLocation(prog, "translate");
293+ glUniform2f(translate, 0.0f, 0.0f);
294+
295+ GLint projection = glGetUniformLocation(prog, "projection");
296+
297+ GLuint tex = generate_target_texture();
298+ glBindTexture(GL_TEXTURE_2D, tex);
299+
300+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
301+ glViewport(0, 0, width, height);
302+
303+ glEnable(GL_BLEND);
304+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
305+
306+ State state =
307+ {
308+ PTHREAD_MUTEX_INITIALIZER,
309+ PTHREAD_COND_INITIALIZER,
310+ true,
311+ true,
312+ {0, {{0,0}}}
313+ };
314+ MirSurface *surface = mir_eglapp_native_surface();
315+ mir_surface_set_event_handler(surface, on_event, &state);
316+
317+ while (mir_eglapp_running())
318+ {
319+ pthread_mutex_lock(&state.mutex);
320+
321+ while (mir_eglapp_running() && !state.changed)
322+ pthread_cond_wait(&state.change, &state.mutex);
323+
324+ if (state.resized)
325+ {
326+ // mir_eglapp_swap_buffers updates the viewport for us...
327+ GLint viewport[4];
328+ glGetIntegerv(GL_VIEWPORT, viewport);
329+ int w = viewport[2], h = viewport[3];
330+
331+ // TRANSPOSED projection matrix to convert from the Mir input
332+ // rectangle {{0,0},{w,h}} to GL screen rectangle {{-1,1},{2,2}}.
333+ GLfloat matrix[16] = {2.0f/w, 0.0f, 0.0f, 0.0f,
334+ 0.0f, -2.0f/h, 0.0f, 0.0f,
335+ 0.0f, 0.0f, 1.0f, 0.0f,
336+ -1.0f, 1.0f, 0.0f, 1.0f};
337+ // Note GL_FALSE: GLES does not support the transpose option
338+ glUniformMatrix4fv(projection, 1, GL_FALSE, matrix);
339+ state.resized = false;
340+ }
341+
342+ glClear(GL_COLOR_BUFFER_BIT);
343+
344+ for (int p = 0; p < state.touch.points; ++p)
345+ {
346+ // Note the 0.5f to convert from pixel middle to corner (GL)
347+ glUniform2f(translate, state.touch.point[p].x + 0.5f,
348+ state.touch.point[p].y + 0.5f);
349+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
350+ }
351+
352+ // Put the event loop back to sleep:
353+ state.changed = false;
354+ pthread_mutex_unlock(&state.mutex);
355+
356+ mir_eglapp_swap_buffers();
357+ }
358+
359+ mir_surface_set_event_handler(surface, NULL, NULL);
360+ mir_eglapp_shutdown();
361+
362+ return 0;
363+}

Subscribers

People subscribed via source and target branches