This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Viking's profile picture

Drawing magic

Started by Viking, 29 January 2016 - 10:01 PM
Viking #1
Posted 29 January 2016 - 11:01 PM
I wanted to open a topic for talking about speeding up ComputerCraft computer's drawing for more enjoyable applications. I am not an expert, but I have a few samples for you. First of all, sorry for the messy code, but it is meant to be a demonstration, not a software architecture tutorial.


I noticed that the term api has a few incomprehensibly slow functions like setTextColor and setBackgroundColor. If I call these functions everytime before drawing a character on the screen then the display is going to be flickering. I think of something like this:


for y = startY, endY do
	for x = startX, endX do
		local bc = getBc(x, y)
		local tc = getTc(x, y)
		local char = getChar(x, y)
		term.setCursorPos(x, y)
		term.setBackgroundColor(bc)
		term.setTextColor(tc)
		tem.write(char)
	end
end

Number of calls with a 51 * 19 characters large display:

setBackgroundColor : 969 (every case)
setTextColor : 969 (every case)
write : 969 (every case)
setCursorPos : 969 (every case)

Erhmm… that's a lot of function calls per screen draw. Accurately 3876. With 20 FPS, that's 77520 (!!)
We should optimize our application somehow. Maybe with fewer function calls, it would be faster, but how can we achieve such a thing? Perhaps it is unnecessary to draw every pixel. Then that means our screen would have "blank pixels". That wouldn't be good. Oh, not. If the renderer draws a pixel at x, y and at the next draw x,y is the same color, same character then it wouldn't be blank. Wait… It lets us be able to draw only the updated pixels, because the pixel drawn in the previous cycle will be perfect for us. One thing is left. How can the computer remember the previously rendered pixels? Right, we have to store them in a buffer. A pretty simple implementation will do the job now, so we can test our new code:


local buffer = Buffer(51, 19)

-- And this goes inside our update loop.
for y = startY, endY do
	for x = startX, endX do
	   local mustRender = false

		-- get colors and characters what should be displayed
		local bc = getBc(x, y)
		local tc = getTc(x, y)
		local char = getChar(x, y)

		-- are the new pixel datas the same as the old? chexk it with simple conditions
		if buffer:getCharAt(x, y) ~= char then
			mustRender = true
		end
		if not mustRender and buffer:textColorAt(x, y) ~= tc then
			mustRender = true
		end
		if not mustRender and buffer:backgroundColor(x, y) ~= bc then
			mustRender = true
		end

		-- it is true if a pixel wasn't the same in the last cycle
		if mustRender then
		   -- save the new pixel datas, otherwise next time the pixel checking algorithm (above) will return true, because we forgot to tell the buffer that we drew the new pixel
		   buffer:updatePixelDatas(x, y, char, tc, bc)
			term.setCursorPos(x, y)
			term.setBackgroundColor(bc)
			term.setTextColor(tc)
			tem.write(char)
		end
	end
end


Huh, this one is a bit more complex, but is it more effective? Well, for the first drawing, it is not faster than the previous implementation, but after keeping a list from the drawn pixels, it won't draw every single pixel every time we draw a tiny black spot onto the monitor. So maybe the number of function calls will be reduced from 3876 to 0. What if we want animations? Is is going to be enough? If you try to move a 4 * 4 characters large cyan rectangle on the screen with a white screen (so everything is white, except the rectangle), then moving it by 1 X in every update will make the program to draw only 8 pixels. (When I say pixels, I refer to CC pixels). However if you move 5 shapes with different colors, it will give you a horrible experience.

Now we got to the point. It doesn't move fast, but why? What makes it so slow if we only draw a few pixels? My answer : setting the text colors and background colors (mostly)
Now everyone knows this, so it will be very easy to implement something what uses less color adjusting.

The solution is the following: Sort the pixels by background colors. The best effect will be achieved by sorting the pixels by background colors. To make it work, we will have to draw in 4 steps.
  1. Checking for updated pixels
  2. Sorting updated pixels by background colors
  3. Drawing the pixels
  4. Updating buffer


local buffer = Buffer(51, 19) -- buffer for the renderer
local sortedBuffer = {} - A table will be good for me now.
-- sort function
function sortBuffer()
	table.sort(sortedBuffer, function(p1 , p2)
		return buffer:getBc(p[1], p[2]) > buffer:getBc(p2[1], p2[2])
	end
end
-- draw function
function drawSortedBufferContent()
	local lastBc = nil
	local lastTc = nil
	for k, v in pairs(sortedBuffer) do
		if lastBc ~= buffer:getBc(v[1], v[2]) then
			lastBc = buffer:getBc(v[1], v[2])
			term.setBackgroundColor(lastBc)
		end
		if lastTc ~= buffer:getTc(v[1], v[2]) then
			lastTc = buffer:getTc(v[1], v[2])
			-- Only has to set text color if the character is a visible letter or symbol, otherwise it will be unwanted and you will ruin performance
			if buffer:getCharAt(v[1], v[2]) ~= " " then
				term.setTextColor(lastTc)
			end
		end
		term.setCursorPos(v[1], v[2])
		term.write(buffer:getCharAt(v[1], v[2])
	end
end
-- the draw behavior inside our app logic loop
for y = startY, endY do
	for x = startX, endX do
	   local mustRender = false
		-- get colors and characters what should be displayed
		local bc = getBc(x, y)
		local tc = getTc(x, y)
		local char = getChar(x, y)
		-- are the new pixel datas the same as the old? check it with simple conditions
		if buffer:charAt(x, y) ~= char then
			mustRender = true
		end
		if not mustRender and buffer:textColorAt(x, y) ~= tc then
			mustRender = true
		end
		if not mustRender and buffer:backgroundColor(x, y) ~= bc then
			mustRender = true
		end
		-- it is true if a pixel wasn't the same in the last cycle
		if mustRender then
			buffer:updatePixelDatas(x, y, char, tc, bc)
			table.insert(sortedBuffer, {
				x,
				y
			})
		end
	end
end
sortBuffer() -- sort after checking pixels
drawSortedBufferContent() -- draw after sorting for better performance

Because the background colors are sorted the program will only set the background color when the next pixel's bc is not the same than the last one's color and this can only happen if there is no more from the specific color. This method reduces the maximum amount of setBackgroundColor to 16 (!!) and this value is resolution dependent (constant), while with the first approach, the maximum number of setBackgroundColor calls was width * height (969 by default for advanved computers). You might noticed that there is an other clever thing for setTextColor. If the drawable character is a whitespace, then the app won't set the text color, because it won't be visible.

Final result?
Number of:
Maximum setBackgroundColor calls: 16
textColor calls : 969, but can be 0, if you don't show characters just whitespaces
setCursorPos calls: 969
write calls: 969

In the best case: About 1900 calls per drawing (whole screen), but usually less, because the programs don't change every single pixel, so with caching it is way more efficient. It is better than the 3876 (constant) we used for the first time what is really similar to the one, beginner programmers create. I hope you've learnt something. At least that you shouldn't listen to me. :D/>


Sorry for all of my mistakes. It is 00:00 here. :D/>
And last, but not least: please leave some techniques here. I'm sure that everyone would learn a lot. :)/>
Edited on 29 January 2016 - 10:32 PM
Lupus590 #2
Posted 29 January 2016 - 11:29 PM
If you clean this up a bit, I can see this being a tutorial for creating/optimising a renderer.
Viking #3
Posted 29 January 2016 - 11:31 PM
That's possible, but give me time for that.
Bomb Bloke #4
Posted 29 January 2016 - 11:40 PM
A couple of notes:

Every time you write to a line, CC redraws the whole line. Assuming the same colours are involved, if you want to write the word "cabbage" over the word "rabbits", then simply writing "cabbage" is faster than writing a "c", moving the cursor, and writing "age".

As of CC 1.74, we've got term.blit(). This magic function can write entire lines of any text/background colour combination at about the same speed as a single term.write() call.
Viking #5
Posted 29 January 2016 - 11:58 PM
Thanks for the suggestion. I will update my post and include term.blit().
Lyqyd #6
Posted 30 January 2016 - 01:02 AM
Yeah, my framebuffer API is pretty fast, and redrawing is done by blitting every line where no knowledge of the previous screen contents is available, or only blitting lines where changes are present when that information is provided, with a fallback to writing the largest contiguous chunks possible using term.write if term.blit is unavailable. You can read through the drawing function if you're interested.
Lupus590 #7
Posted 30 January 2016 - 12:17 PM
has anyone made any comparisons to the various frame buffers? it may be interesting to see which ones perform best in different situations
Edited on 30 January 2016 - 11:18 AM
Bomb Bloke #8
Posted 30 January 2016 - 12:48 PM
It'd certainly depend on the situation. For example, the one my GIF API uses is pretty fast - even on pre-term.blit() builds of ComputerCraft, where it defines itself an alternative function to use - but part of its speed comes from only paying attention to background colours (as the only characters it ever actually writes are spaces). It's basically a super-dumbed-down version of the window API (being about 50 lines), producing terminal objects that only support blit(), setCursorPos(), clear(), and a custom function that actually makes them render their contents (the other functions don't). Main reason it exists is to allow partial buffer redraws.

That's really the only problem I have with the main window API these days - if you want to redraw a window's contents, then you have to redraw all of it. You also have no way of looking at the content in the buffer, but while that bugs me, I admit that I can never think up a good excuse as to why you might ever need to…

It's also worth noting that you can potentially improve your performance just by drawing to a window set to invisible, then make that window visible for just a moment before you continue rendering. Every time you make the window visible it draws its whole content (taking about the same amount of time as term.clear(), regardless as to the complexity of what's in it), so by "queuing" a bunch of draw operations into an invisible window (which stores what you write into it, but doesn't actually pass it on to the main terminal display), you can take a simple shortcut past optimising your own code.
Edited on 30 January 2016 - 11:53 AM
MKlegoman357 #9
Posted 30 January 2016 - 01:28 PM
Just wondering, did you make any benchmark tests with setting colors? Last time I've tested the term functions I noticed that it's actually only the writing functions which cause lag, like term.write and term.blit, and color setting functions didn't really cause any additional lag. Of course, having as less function calls as possible is better.

Because of my tests I came to the conclusion that (without having access to term.blit) whenever you want to draw a, for example, white line with a gray smaller line in the middle, instead of drawing a white line, then a gray line and then a white line again, you'd draw the whole line white and then another one in the middle. Here's a code example:


--# this is slower
term.setCursorPos(1, 1)
term.setBackgroundColor(colors.white)
term.write(string.rep(" ", 4))
term.setBackgroundColor(colors.gray)
term.write(string.rep(" ", 4))
term.setBackgroundColor(colors.white)
term.write(string.rep(" ", 4))

--# this is faster
term.setCursorPos(1, 1)
term.setBackgroundColor(colors.white)
term.write(string.rep(" ", 12))
term.setCursorPos(5, 1)
term.setBackgroundColor(colors.gray)
term.write(string.rep(" ", 4))
Bomb Bloke #10
Posted 30 January 2016 - 03:23 PM
Catch is a real buffer would have its work cut out figuring how to apply the technique. You'd want to build and arrange an index of some sort every time something got written into it.
SoftNougat #11
Posted 29 February 2016 - 01:38 AM
Guys , Thanks for sharing some useful notes and techniques here.
HDeffo #12
Posted 29 February 2016 - 03:41 AM
I did a benchmark a long time ago of various buffers I don't have it anymore but lyqyd might. I took optimizations from each one as well as a few of my own and improved the speed of his buffer by about 40% that being said this does give a general idea on possibly greater improvements. I may look into this again when I'm not swamped in millions of my projects.
Bomb Bloke #13
Posted 29 February 2016 - 06:45 AM
This might be useful, if you already have a selection of buffers in mind to test through it.