Click here to Skip to main content
15,917,618 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
Disclaimer: I can't easily make the code in "What I have tried" line wrap on a display this narrow fixed layout. It might help to view it in fluid mode?

I've got a problem where I'm lost in rectangles. The issue (at least unless I'm missing something else as well) seems to be I just have subpar spatial reasoning abilities and I can't offset my rectangles correctly.

Here's what I am doing. I have a "screen" with a background color and a number of "controls".

I have a small fixed buffer (usually 32kB or less) that can be rendered at a time. For RGB565 16-bit color this is 2 bytes per pixel, while a screen might be, say 480x320.

Each control has a valid() flag that indicates whether it needs to be re-rendered, while visible() indicates whether it should be drawn at all.

When each control is rendered, its logical coordinates are upper left (0,0), so I have a control_surface wrapper that offsets all draws to an underlying bitmap it holds.

First I find dirty rectangles. This is easy and works, though I'm sure it could be optimized.

Then for each dirty rectangle, I create a bitmap over the buffer that's the width of the rectangle and however many lines in height I can hold in the memory I have to work with. I also create a subrectangle sr that represents that subset of the dirty rectangle. This is because my maximum usable memory buffer may not be large enough to permit me to render all of the dirty rectangle at once. In this case it moves from top to bottom of the dirty rect, rendering that portion of the rect. sr represents the subset of the dirty rectangle being rendered. Note that in cases where a control overlaps multiple subrects, it will be rendered more than once.

Then for each subrectangle, I go through any controls that are visible() and whose bounds() intersect it.

I then try to create the logical offset for the control, and this is where I can't seem to get it right. I can get the topmost/first render iteration to work but successive renders do not. Even as I offset the logical coordinates of the control surface as y increases it still doesn't want to work for me at all. My offsets are always wrong.

I didn't put everything I've tried in the tried section, because I've been at this for a day and a half. Instead the code for the primary render routine is there. I'm available to answer questions about it because I know it's a lot to take in. As far as I can tell most of it works. The // TODO: is the part I'm struggling with.

What I have tried:

C++
uix_result render(render_callback_type render_callback,void* state = nullptr, size_t max_memory = 32*1024, uint8_t* render_buffer = nullptr, void*(allocator)(size_t size)=::malloc,void(deallocator)(void*)=::free) {
    data::simple_vector<rect16> dirties;
    if(!m_valid) {
        // invalidate the entire screen
        dirties.push_back(bounds());
    } else {
        // go through all the controls
        for(control_type** it = m_controls.begin();it!=m_controls.end();++it) {
            // it is double indirected - make it easier to use
            const control_type *pit = *it;
            // only care about controls that must be redrawn
            if(!pit->valid() && pit->visible()) {
                rect16 r = (rect16)pit->bounds().crop((srect16)this->bounds());
                rect16* pr = &r;
                // do we already have a rectangle that contains this rectangle,
                // or otherwise does this rectangle contain another rectangle
                // in the list?
                if(is_already_contained(dirties,pr)) {
                    if(pr!=nullptr) {
                        // r contains an existing rectangle
                        pr->x1 = r.x1;
                        pr->y1 = r.y1;
                        pr->x2 = r.x2;
                        pr->y2 = r.y2;
                    } 
                } else {
                    // add the main rect to the list
                    dirties.push_back(r);
                }
            }
        }
    }
    if(dirties.size()==0) {
        // nothing more to do
        return uix_result::success;
    }
    Serial.printf("Found %d dirty rects.\n",(int)dirties.size());
    uint32_t max_area = 0;
    rect16 largest_rect;
    // find the largest rect
    for(const rect16* it = dirties.cbegin();it!=dirties.cend();++it) {
        uint32_t area = it->width()*it->height();
        if(area>max_area) {
            max_area = area;
            largest_rect = *it;
        }
    }
    // total target size of bitmap in bytes - start with the same size as the largest_rect
    size_t bmp_size = bitmap_type::sizeof_buffer(largest_rect.dimensions());
    // total size in bytes of one line, padded to byte boundaries
    size_t bmp_stride_size = bitmap_type::sizeof_buffer({largest_rect.width(),1});
    // max memory must hold at least a single line
    if(max_memory<bmp_stride_size) {
        return uix_result::invalid_argument;
    }
    // if the size is too big for max_memory, take the biggest size we can.
    if(bmp_size>max_memory) {
        bmp_size = bitmap_type::sizeof_buffer({largest_rect.width(),uint16_t(max_memory/bmp_stride_size)});
    }
    // our bitmap buffer can be passed on, or otherwise it will be allocated here
    // bmp_data holds the pointer either way.
    uint8_t* bmp_data = m_render_buffer!=nullptr?m_render_buffer:(uint8_t*)allocator(bmp_size);
    // the following only happens if allocator above failed:
    // if we can't allocate it all keep halving the amount to try
    // until we succeed - as long as the size is >= one line
    while(bmp_data==nullptr && bmp_size>bmp_stride_size) {
        bmp_size/=2;
        bmp_data = (uint8_t*)allocator(bmp_size);
    }
    // if we still couldn't allocate return out of memory
    if(bmp_data==nullptr) {
        return uix_result::out_of_memory;
    }
    Serial.println("Allocated bitmap render buffer");
    // for each dirty rectangle 
    for(const rect16* it = dirties.cbegin();it!=dirties.cend();++it) {
        rect16 r = *it;
        Serial.printf("Dirty rect is (%d,%d)-(%d,%d)\n",(int)r.x1,(int)r.y1,(int)r.x2,(int)r.y2);
        // get the size of the dirty rect in bytes
        size_t r_size = bitmap_type::sizeof_buffer(r.dimensions());
        // compute how many lines of it we can hold at once
        size_t lines = bmp_size/bitmap_type::sizeof_buffer({r.width(),1});
        Serial.printf("Allocated %d lines.\n",(int)lines);
        // wrap bmp_data with a bitmap_type instance
        // and init with screen color
        bitmap_type bmp({r.width(),(uint16_t)lines},bmp_data);
        bmp.fill(bmp.bounds(),m_background_color);
        int h = (int)r.height();
        // do for each set of lines
        for(int y = 0;y<h;y+=lines) {
            // compute the subrectangle of this dirty rectangle
            // this is what we render to (physical coordinates)
            srect16 sr(r.x1,r.y1+y,r.x2,r.y1+lines+y);
            Serial.printf("sr subrect is (%d,%d)-(%d,%d)\n",(int)sr.x1,(int)sr.y1,(int)sr.x2,(int)sr.y2);
            bool found = false;
            // for each control
            for(control_type** cit = m_controls.begin();cit!=m_controls.end();++cit) {
                control_type* pcit = *cit;
                srect16 r_offset = pcit->bounds();
                // does this control lie within the dirty rect?
                if(pcit->visible() && r_offset.intersects(sr)) {
                    // found a control
                    found = true;
                    // crop our working rect to the subrect
                    r_offset = r_offset.crop(sr);
                    // now offset it by the subrect to set the logical coordinates
                    // of the upper left of the control surface to (0,0)
                    r_offset.offset_inplace(-sr.x1,-sr.y1);
                    // TODO: further modify the rect so as to make sure
                    // that when y gets incremented we shift the whole thing up
                    // such that we're rending over and over as we move through
                    // successive segments of the control.
                    Serial.printf("Offset rect is (%d,%d)-(%d,%d)\n",(int)r_offset.x1,(int)r_offset.y1,(int)r_offset.x2,(int)r_offset.y2);
                    // establish the bitmap backed control surface at the proper logical coordinates
                    control_surface_type surface(bmp,r_offset);
                    // render the control
                    pcit->on_render(surface);
                }
            }
            // send the bitmap the display
            render_callback({r.x1,uint16_t(r.y1+y)},bmp,state);
            // erase the background for the next iteration
            if(y<h && found) {
                bmp.fill(bmp.bounds(),m_background_color);
            }
        }
    }
    if(m_render_buffer!=nullptr) {
        deallocator(bmp_data);
    }
    m_valid = true;
    return uix_result::success;
}
Posted
Updated 22-Feb-23 3:41am
v2
Comments
Kenneth Haugland 22-Feb-23 1:12am    
I gonna need a LOT of coffee to even attempt to look at this :)
honey the codewitch 22-Feb-23 1:20am    
Yeah. I understand. I just ate a BLT and intend to nap. Maybe a solution will present itself that way.
[no name] 22-Feb-23 1:52am    
The more complex my code, the more (debug) asserts I use to confirm and question my assumptions; particularly when stepping is tedious.
honey the codewitch 22-Feb-23 6:23am    
The problem with asserts on this platform is you don't get a reasonable debugger (JTAG makes you want to get out and push) - asserts will create a boot loop which actually makes things harder to debug. Better to dump to the serial port, or failing that, while(1); the damned thing after dumping a message to serial. I'm dumping to the serial for this, but I'm not 100% sure what to expect so I have nothing to feed an assert.
[no name] 22-Feb-23 12:19pm    
Maybe you can tackle the problem in terms of (connected) "lines". Any line should fit the buffer. The lines are hidden or dirty, etc. I even use references like "top" (side) and bottom, etc. if it keeps my head straight in a given context (rotating).

1 solution

Your render rectangle is one pixel too large:

srect16 sr(r.x1,r.y1+y,r.x2,r.y1+lines+y);

will create a window that is lines+1 pixels high (assuming these rectangles are top and bottom pixels.) I guess you intended:
srect16 sr(r.x1,r.y1+y,r.x2,r.y1+lines-1+y);


If I understand the code correctly you simple need to offset the render position by the y offset, so your TODO is:

r_offset.offset_inplace(0,-y);


Your comment after the TODO seems to confirm this... you already answered this yourself!

Good luck.
 
Share this answer
 
Comments
honey the codewitch 22-Feb-23 10:14am    
I'm keeping this solution open pending further investigation because I've tried it and it's not working but now I think the bug is probably somewhere else. Thanks for finding that 1 pixel bug =) Fencepost errors are irritating.
RainHat 22-Feb-23 11:18am    
That's fair.

Have you put dummy data in the second call to render_callback, where y is not 0 to check that is rendering to the correct place.

I am as in the dark as you are with this one - the code looks good to me.
honey the codewitch 22-Feb-23 14:17pm    
As best as I could visually verify yes. I did similar. I'm closing this solution as solved because I'm near certain the bug is coming from my rendering routines. Maybe deep in my graphics library, brought about because of negative coordinates off the top of the bitmap. It *should* clip, and draw appropriately but it seems it's not. I think it might be due to converting something from signed to unsigned without cropping it first.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900