Smooth scrolling a 4K screen

I’m scrolling a 4K image horizontally but it isn’t as smooth as I would like. I’m wondering if there is a better technique ?

My current approach is to load up a texture that is two screens wide, then render the texture so that it progresses across the screen. I presume OpenGL and the hardware handles the render cropping with ease.

It is a big texture: 16M pixels and running on very modest Intel Xe GPU rendering to a 30Hz screen.

The result is slow or jerky. At a 30Hz refresh rate moving one pixel a frame is obviously slow. Is there some “illusionistic” approach that might make the result seem both faster and smoother ?

Well, that’s going to depend on your GPU (Intel Xe), your GPU driver, and how well you’re utilizing it via OpenGL and whatever window system interface you’re using (WGL, GLX, EGL, etc.)

So 4096 x 4096 … or something.
30Hz is kinda slow. Is that the rate you want?

So it’s slower than 30Hz? Dag. What’s your app doing?

You’re going to have to dig in and figure out what your app is doing to make it that slow.

  • Have you tried a texture that is 1/4 or 1/16th the size? How does that affect your frame time?
  • What GL internal format are you using for your texture?
  • Does your texture have MIPmaps and are you telling the GPU to use them?
  • What is your window resolution?
  • And you’ve got it double-buffered, right?
  • VSync ON or OFF? For testing, try flipping it.
  • Try getting rid of the texture render and just clear the window to increasing colors and swap buffers? How’s the frame time on that?
  • How sure are you that you are actually using the GPU for rendering, and not some GL software emulation driver like Mesa3D w/o GPU acceleration enabled?
  • What system and CPU is this on? Let’s see your GL_RENDERER and GL_VERSION strings.

It’s worth keeping in mind that that Intel Xe is going to be using slow system RAM for both texture storage and render target storage. So taking potshots at a large 4k x 4k texture every frame to render a large window is going to hit mem B/W much harder on your system than it would a discrete GPU with its own fast on-board VRAM. But still. The frame rate you’re getting suggests that there might be something else going on here too.

Thank you for your response. The ‘application’ is an artwork working on a low-end CPU & GPU. The platform is fixed. The display is a basic 42 inch 4K TV with a 30Hz refresh. All basic and inexpensive hardware. This is not a game.

The image is 3,840 x 2 = 7,680 by 2,160 pixels and is loaded on to OpenGL (via Mesa) as one large texture and rendered with X11. Rendering every pixel move thus requires 3,480 renderings to slide the new, half image on screen.

I’m looking for an illusion, not new hardware.

Ok. Well, you didn’t answer any of my questions, so there’s not much I can offer.

That said, “you’re” going to have to answer those questions if/when you decide to optimize your application for good performance.

My bet is that you’re not even using your embedded GPU, and instead are rendering your textured quad on the CPU using Mesa3D’s software rasterizer (LLVMpipe or Softpipe). So you shouldn’t expect great performance, especially on a “low-end CPU & GPU”, with a large texture, and with possibly less than optimal use of the OpenGL API. That’s just a guess though. Feel free to prove me wrong!

The GPU is being used and appropriate shaders are set up with the necessary matrices. The image is loaded as a single texture and rendered via glDrawArrays(GL_QUADS, 0, 4). The artwork is using Intel Xe Graphics with Intel Mesa 22.2. obtained from Oibaf.

I have to say you seem a little hostile. Most of your questions were not appropriate to this issue, such as: “Have you tried a texture that is 1/4 or 1/16th the size?” – I’m dealing with full screen rendering. Making the problem smaller will of course speed things up – but that isn’t the issue.

Not meaning to be. I’d like to give you some useful tips here to help troubleshoot your performance problem (and I’m probably not the only one here). But if you’re not going to provide the important details of how you decided to use OpenGL to address your rendering task, then there’s very little we can do to help you.

You have to profile your code. You have to determine the primary bottleneck. You need to determine the cause of this primary bottleneck. And then you need to determine what options are available to you to fix it. You apparently haven’t done any of this. You need to do this.

I can appreciate that you might think so, given your current level of rendering experience. But nevertheless they may be very important, depending on how you decided to make use of OpenGL and what OpenGL driver support you’re actually using under-the-hood. Much of this we still don’t know yet.

1 Like

So here are the layers that create and load the texture and it’s subsequent rendering:

/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
void generate_and_bind_opengl_object(GLint type, GLuint *object_id)
{
	*object_id = 0;

	switch(type)
	{
		case GL_ARRAY_BUFFER:
		case GL_ELEMENT_ARRAY_BUFFER:
		case GL_PIXEL_PACK_BUFFER:
		case GL_PIXEL_UNPACK_BUFFER:
		case GL_TRANSFORM_FEEDBACK_BUFFER:
			glGenBuffers(1, object_id);
			glBindBuffer(type, *object_id);
		break;

		case GL_FRAMEBUFFER:
			glGenFramebuffers(1, object_id);
			glBindFramebuffer(type, *object_id);
		break;

		case GL_RENDERBUFFER:
			glGenRenderbuffers(1, object_id);
			glBindRenderbuffer(type, *object_id);
		break;

		case GL_TEXTURE_1D:
		case GL_TEXTURE_2D:
		case GL_TEXTURE_3D:
		case GL_TEXTURE_1D_ARRAY:
		case GL_TEXTURE_2D_ARRAY:
		case GL_TEXTURE_CUBE_MAP:
			glGenTextures(1, object_id);
			glBindTexture(type, *object_id);
			OpenGL_error_check(__FILE__, __LINE__, __FUNC__);
		break;

		case GL_VERTEX_ARRAY:
			glGenVertexArrays(1, object_id);
			glBindVertexArray(*object_id);	// note the different parameters
		break;
		default:
			program_error(__FILE__, __LINE__, __FUNC__,
				"unrecognized object type: %d", type);
		break;
	}

	if(*object_id == 0)
	{
		program_error(__FILE__, __LINE__, __FUNC__,
			"generating and binding opengl object failed type: %d", type);
	}

	OpenGL_error_check(__FILE__, __LINE__, __FUNC__);
}
/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
void textureize_image_to_opengl(IMAGE *image)
{
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	// Reference the entire image
	glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); // restore skipping to zero
	glPixelStorei(GL_UNPACK_SKIP_ROWS,   0);
	glPixelStorei(GL_UNPACK_ROW_LENGTH,  0); // zero means full width!

	glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

	// texturize the 0-th level mipmap
	glTexImage2D
	(
		GL_TEXTURE_2D,
		0,   // the 0-th level mipmap
		GL_RGBA,
		image->width, image->length,
		0,  // NO borders
		GL_RGBA, GL_UNSIGNED_BYTE, image->buffer
	);

	OpenGL_error_check(__FILE__, __LINE__, __FUNC__);
}
/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
void generate_bind_textureize_and_activate_opengl_texture(IMAGE *image)
{
	if(image->texture_id != 0)
	{
		// there is a texture already associated with the image
		// which should to be deleted
		glDeleteTextures(1, &image->texture_id);
	}

	generate_and_bind_opengl_object(GL_TEXTURE_2D, &image->texture_id);
	textureize_image_to_opengl(image);
	glActiveTexture(IMAGE_SAMPLER2D + GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, image->texture_id);
}
/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
void set_uniform_variable_h4_matrix
(
	const char *variable,
	bool transpose,
	GLfloat *h4_matrix
)
{
	GLint location, program_id;

	glGetIntegerv(GL_CURRENT_PROGRAM, &program_id);	// this is boring !
	location = glGetUniformLocation(program_id, variable);

	if(location < 0)
	{
		OpenGL_status_report(__FILE__, __LINE__, __FUNC__,
			"failed to locate (to set) uniform variable: '%s'\n\tin program %d",
			variable, program_id);
	}

	glUniformMatrix4fv
	(
		location,	// GLint variable location
		1,			// GLsizei count only 1
		transpose,	// GLboolean transpose / don't transpose the matrix?
		h4_matrix	// GLfloat * pointer to float matrix data
	);
}
/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
void OpenGL_render_matrix_inverted_image(IMAGE *image)
{
	float h4_translation_matrix[16],	h4_rotation_matrix[16];
	float h4_scale_matrix[16],			h4_ortho_matrix[16];
	float x, y;
	struct timeval us_start_time;

	// operate on the lower left corner of the image
	x = image->x_pos;
	y = image->y_pos;

	compute_ortho_matrix_h4(h4_ortho_matrix);
	compute_translate_matrix_h4(x, y, h4_translation_matrix);
	compute_rotate_matrix_h4(image->dx, image->dy, h4_rotation_matrix);
	vertically_reflect_matrix_h4(h4_rotation_matrix);	// reflect vertically
	compute_scale_matrix_h4(image->width * image->scale, image->length * image->scale, h4_scale_matrix);

	set_uniform_variable_h4_matrix("translation_matrix",	TRUE,	h4_translation_matrix);
	set_uniform_variable_h4_matrix("rotation_matrix",		FALSE,	h4_rotation_matrix);
	set_uniform_variable_h4_matrix("scale_matrix",			FALSE,	h4_scale_matrix);

	glDrawArrays(GL_QUADS, 0, 4);
}
/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/
	// The following is done just once to load the slider image as a texture
	// the slider image is 7,680 pixels wide and 2,160 pixels high
		generate_bind_textureize_and_activate_opengl_texture(slider);

	// The folowing is done 3,840 times to slide the existing image off screen and the
	// new image on screen
		OpenGL_render_matrix_inverted_image(slider);

/*------------------------------------------------------------------------------

------------------------------------------------------------------------------*/

It is a while since I ran a profile. It has always shown that the GPU is the hotspot.


The Google profiler is really good.

You asked this but, the very title of my query is Smooth scrolling a 4K screen. I presume you know what a 4K screen is ?

To scroll the whole screen requires rendering the texture 3,840 times. At 30Hz that would take 128 seconds if the scroll is one pixel per frame. One can, of course, shift more than one pixel per frame but things no longer look smooth.

For my artwork 128 seconds is too slow. The 30Hz frame rate is dictated by cheap 4K TV screens.

I am currently shifting the texture by 1.5 pixels in the hope that the GPU will fudge the location in some smart way. Things do go faster and are smooth enough. With bigger shifts things look jerky.

If the shifts are really big then the eye tends to see the movement as fast but continuous motion. However, that is closer to a wipe than I want. Is there an optical “trick” to fool the eye, between these two extremes.

So this isn’t a question about performance, about the actual time it takes to render anything. It’s purely a question of getting “smooth” visuals on a low-refresh rate display.

One problem is that it’s not clear what “jerky” really means, objectively speaking. A lot of people using 30Hz displays would see moving 2 or even 4 pixels per frame as reasonably smooth. Or at least, as smooth as anything else they see on such a display device. Scrolling through web pages or whatever isn’t limited to 1 pixel per second movement. As such, any “jerkiness” is something they’re just used to.

Basically, don’t worry about it. Pick a scrolling speed based on how fast you want it to move, and accept that it will be less smooth on some screens than others.

However, if you absolutely insist on making it appear smoother on a display device where such smoothness is not possible, you can effectively apply motion-blur to the texture. Essentially, your shader samples the texture in multiple places and combines all of those colors to produce the final pixel.

Ahh. Motion-blur. That seems like a thing to try. An illusionistic approach.

It is in an artwork so the aesthetics are everything. I’m the kind of person that finds rapid panning of scenes in a movie annoying because I can see the ‘jumps’.

Thank you.