Found the source of the problem. The vte_terminal_process_incoming function from vte.c keeps track of a "bounding box" of cells to be invalidated, and uses that to invalidate cells at points such as after all input has been processed, or when a large enough cursor jump is made (to avoid letting the bounding box get needlessly large). This bounding box is represented in terms of the total number of lines in the terminal, including history. The problem that arises is that if a scroll takes place before the bounding box has been used to invalidate cells, then a new row is added to the total terminal rows, increasing the index number of the bottom rows. Thus, the bounding box will now be off by one (or however large the scroll is), and no longer reaches all the way to the bottom of the screen (if it did before). This problem applies, even when no scrollback history is enabled, as the relevant indexes all still increase, even though the true number of actual data rows hasn't changed.
The fix I implemented is to force invalidation to take place if we move into a scroll region from outside it.