2
0
mirror of https://github.com/koreader/koreader synced 2024-11-16 06:12:56 +00:00

ReaderRolling: quicker partial rerenderings with EPUBs

Only available with EPUBs containing 2 or more fragments,
and a file size large enough to ensure a cache file is used.
The idea is simply, on any rendering setting change, to
skip the rerendering of the full book and to defer any
rerendering to the moment we draw a DocFragment, and
render only it.
So, on a setting change, only the fragment containing the
current page will be rerendered, and the new fragments we
may cross while turning pages.
When having done so, KOReader is in a degraded state (the
full page count is incorrect, the ToC is invalid...).
So, a full rerendering is needed, and one will happen
in the background, and when the user is idle, we reload
seamlessly and quickly from the cache file it has made.
ReaderFlipping will show some icons in the top left
corner to let it know at which steps in this procress
we are.
This commit is contained in:
poire-z 2023-02-16 23:30:58 +01:00
parent 15499434df
commit 81f2aed086
8 changed files with 615 additions and 15 deletions

View File

@ -6,6 +6,13 @@ local Screen = require("device").screen
local ReaderFlipping = WidgetContainer:extend{
orig_reflow_mode = 0,
-- Icons to show during crengine partial rerendering automation
rolling_rendering_state_icons = {
PARTIALLY_RERENDERED = "cre.render.partial",
FULL_RENDERING_IN_BACKGROUND = "cre.render.working",
FULL_RENDERING_READY = "cre.render.ready",
RELOADING_DOCUMENT = "cre.render.reload",
},
}
function ReaderFlipping:init()
@ -38,11 +45,54 @@ function ReaderFlipping:resetLayout()
self[1].dimen.w = new_screen_width
end
function ReaderFlipping:getRollingRenderingStateIconWidget()
if not self.rolling_rendering_state_widgets then
self.rolling_rendering_state_widgets = {}
end
local widget = self.rolling_rendering_state_widgets[self.ui.rolling.rendering_state]
if widget == nil then -- not met yet
local icon_size = Screen:scaleBySize(32)
for k, v in pairs(self.ui.rolling.RENDERING_STATE) do -- known states
if v == self.ui.rolling.rendering_state then -- current state
local icon = self.rolling_rendering_state_icons[k] -- our icon (or none) for this state
if icon then
self.rolling_rendering_state_widgets[v] = IconWidget:new{
icon = icon,
width = icon_size,
height = icon_size,
alpha = not self.ui.rolling.cre_top_bar_enabled,
-- if top status bar enabled, have them opaque, as they
-- will be displayed over the bar
-- otherwise, keep their alpha so some bits of text is
-- visible if displayed over the text when small margins
}
else
self.rolling_rendering_state_widgets[v] = false
end
break
end
end
widget = self.rolling_rendering_state_widgets[self.ui.rolling.rendering_state]
end
return widget or nil -- return nil if cached widget is false
end
function ReaderFlipping:onSetStatusLine()
-- Reset these widgets: we want new ones with proper alpha/opaque
self.rolling_rendering_state_widgets = nil
end
function ReaderFlipping:paintTo(bb, x, y)
if self.ui.highlight.select_mode then
if self[1][1] ~= self.select_mode_widget then
self[1][1] = self.select_mode_widget
end
elseif self.ui.rolling and self.ui.rolling.rendering_state then
local widget = self:getRollingRenderingStateIconWidget()
if self[1][1] ~= widget then
self[1][1] = widget
end
if not widget then return end -- nothing to get painted
else
if self[1][1] ~= self.flipping_widget then
self[1][1] = self.flipping_widget

View File

@ -9,6 +9,7 @@ local ReaderPanning = require("apps/reader/modules/readerpanning")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local bit = require("bit")
local ffiutil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
@ -19,6 +20,18 @@ local T = require("ffi/util").template
local band = bit.band
-- We need a small mmap'ped segment to exchange states with forked
-- subproceses doing background rerenderings.
-- Used as:
-- shared_state[0] = pid of current subprocess
-- shared_state[1] = 0 or 1, set by subprocess when rendering done, waiting to save cache
-- shared_state[2] = 0 or 1, set by main process when subprocess can go on saving cache
local ffi = require("ffi")
local shared_state_data = ffi.C.mmap(nil, 3*ffi.sizeof("uint32_t"), bit.bor(ffi.C.PROT_READ, ffi.C.PROT_WRITE),
bit.bor(ffi.C.MAP_SHARED, ffi.C.MAP_ANONYMOUS), -1, 0)
local shared_state = ffi.cast("uint32_t*", shared_state_data)
local koreader_pid = ffi.C.getpid()
--[[
Rolling is just like paging in page-based documents except that
sometimes (in scroll mode) there is no concept of page number to indicate
@ -54,6 +67,18 @@ local ReaderRolling = InputContainer:extend{
-- same page when turning first 2-pages set of document)
odd_or_even_first_page = 1, -- 1 = odd, 2 = even, nil or others = free
hide_nonlinear_flows = nil,
partial_rerendering = false,
nb_partial_rerenderings = 0,
rendering_state = nil,
RENDERING_STATE = {
FULLY_RENDERED = nil,
PARTIALLY_RERENDERED = 1,
FULL_RENDERING_IN_BACKGROUND = 2,
FULL_RENDERING_READY = 3,
RELOADING_DOCUMENT = 4,
DO_RELOAD_DOCUMENT = 5,
}
}
function ReaderRolling:init()
@ -61,8 +86,16 @@ function ReaderRolling:init()
self.pan_interval = time.s(1 / self.pan_rate)
table.insert(self.ui.postInitCallback, function()
self.rendering_hash = self.ui.document:getDocumentRenderingHash()
self.rendering_hash = self.ui.document:getDocumentRenderingHash(true)
self.ui.document:_readMetadata()
if self.ui.document:hasCacheFile() and not self.ui.document:isCacheFileStale() then
-- We loaded from a valid cache file: remember its hash. It may allow not
-- having to do any background rerendering if the user somehow reverted
-- some setting changes before any background rerendering had completed
-- (ie. with autorotation, transitionning from portrait to landscape for
-- a few seconds, to then end up back in portrait).
self.valid_cache_rendering_hash = self.ui.document:getDocumentRenderingHash(false)
end
end)
table.insert(self.ui.postReaderCallback, function()
self:updatePos()
@ -258,6 +291,13 @@ function ReaderRolling:onReadSettings(config)
end
self.ui.document:setHideNonlinearFlows(self.hide_nonlinear_flows)
-- Will be activated on ReaderReady
if config:has("partial_rerendering") then
self.partial_rerendering = config:isTrue("partial_rerendering")
else
self.partial_rerendering = G_reader_settings:nilOrTrue("cre_partial_rerendering")
end
-- Set a callback to allow showing load and rendering progress
-- (this callback will be cleaned up by cre.cpp closeDocument(),
-- no need to handle it in :onCloseDocument() here.)
@ -275,14 +315,19 @@ end
-- in scroll mode percent_finished must be save before close document
-- we cannot do it in onSaveSettings() because getLastPercent() uses self.ui.document
function ReaderRolling:onCloseDocument()
self:tearDownRerenderingAutomation()
self.current_header_height = nil -- show unload progress bar at top
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
local cache_file_path = self.ui.document:getCacheFilePath() -- nil if no cache file
self.ui.doc_settings:saveSetting("cache_file_path", cache_file_path)
if self.ui.document:hasCacheFile() then
-- also checks if DOM is coherent with styles; if not, invalidate the
-- cache, so a new DOM is built on next opening
if self.ui.document:isBuiltDomStale() then
-- Don't check if we are reloading from a cache built in background: if
-- incoherent, the user will get the popup after the re-open, and can
-- decide to reload, or just revert his changes and avoid the reload.
if self.ui.document:isBuiltDomStale() and self.rendering_state ~= self.RENDERING_STATE.DO_RELOAD_DOCUMENT then
logger.dbg("cre DOM may not be in sync with styles, invalidating cache file for a full reload at next opening")
self.ui.document:invalidateCacheFile()
end
@ -330,6 +375,7 @@ function ReaderRolling:onSaveSettings()
end
self.ui.doc_settings:saveSetting("visible_pages", self.visible_pages)
self.ui.doc_settings:saveSetting("hide_nonlinear_flows", self.hide_nonlinear_flows)
self.ui.doc_settings:saveSetting("partial_rerendering", self.partial_rerendering)
end
function ReaderRolling:onReaderReady()
@ -338,6 +384,13 @@ function ReaderRolling:onReaderReady()
self.ui.document:cacheFlows()
end
self.setupXpointer()
if self.partial_rerendering then
UIManager:nextTick(function()
if self.ui.document then -- (could have disappeared with unit tests)
self.ui.document:enablePartialRerendering(true)
end
end)
end
end
function ReaderRolling:setupTouchZones()
@ -423,6 +476,63 @@ function ReaderRolling:addToMainMenu(menu_items)
help_text = hide_nonlinear_text,
}
end
menu_items.partial_rerendering = {
text = _("Enable partial renderings"),
enabled_func = function()
return self.ui.document:canBePartiallyRerendered() == true
end,
checked_func = function()
return self.ui.document:isPartialRerenderingEnabled() == true
end,
callback = function()
if self.ui.document:isPartialRerenderingEnabled() then
-- (Don't disable it if we are currently in a rerendering automation)
if not self.rendering_state then
self.partial_rerendering = false
if self.ui.document:enablePartialRerendering(false) then
-- Disabling returns true when some partial rerenderings had been done.
-- A full rerendering is needed to have a properly rendered document.
self:onUpdatePos()
end
end
else
self.ui.document:enablePartialRerendering(true)
self.partial_rerendering = self.ui.document:isPartialRerenderingEnabled()
end
end,
hold_callback = function()
local cre_partial_rerendering = G_reader_settings:nilOrTrue("cre_partial_rerendering")
local text = _([[
With EPUB documents (having multiple fragments), text appearance adjustments can be made quicker by only rendering the current chapter.
After such partial renderings, the book and KOReader are in a degraded state: you can turn pages, but some info and features may be broken or disabled (ie. footer info, ToC, statistics).
To get back to a sane state, a full rendering will happen in the background, get cached, and the document will be seamlessly reloaded after a brief period of inactivity.]])
if cre_partial_rerendering then
text = text .. "\n\n" .. _("The current default (★) is to enable partial renderings when possible.")
else
text = text .. "\n\n" .. _("The current default (★) is to always do a full rendering.")
end
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
UIManager:show(MultiConfirmBox:new{
text = text,
-- This text is a bit long, and MultiConfirmBox currently doesn't adjust the
-- font size and may overflow the screen height: use a smaller font size
face = require("ui/font"):getFace("infofont", 20),
icon = "cre.render.partial",
choice1_text_func = function()
return cre_partial_rerendering and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("cre_partial_rerendering")
end,
choice2_text_func = function()
return cre_partial_rerendering and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("cre_partial_rerendering")
end,
})
end,
}
end
function ReaderRolling:getLastPercent()
@ -951,10 +1061,19 @@ function ReaderRolling:updatePos(force)
return
end
-- Check if the document has been re-rendered
local new_rendering_hash = self.ui.document:getDocumentRenderingHash()
local new_rendering_hash = self.ui.document:getDocumentRenderingHash(true)
if new_rendering_hash ~= self.rendering_hash or force then
logger.dbg("rendering hash changed:", self.rendering_hash, ">", new_rendering_hash)
self.rendering_hash = new_rendering_hash
if self.ui.document:isRerenderingDelayed(true) then
-- Partial rerendering is enabled, rerendering is delayed
logger.dbg(" but rendering delayed, will do partial renderings on draw")
self:handleRenderingDelayed()
return
end
-- Full rerendering done.
-- A few things like page numbers may have changed
self.ui.document:resetCallCache() -- be really sure this cache is reset
self.ui.document:_readMetadata() -- get updated document height and nb of pages
@ -1144,6 +1263,12 @@ function ReaderRolling:onSetVisiblePages(visible_pages)
self.ui.document:setVisiblePageCount(visible_pages)
local cur_visible_pages = self.ui.document:getVisiblePageCount()
if cur_visible_pages ~= prev_visible_pages then
if self.ui.document:isPartialRerenderingEnabled() then
-- Page numbers have gone *2 or /2, we don't want drawCurrentViewByPage()
-- to ensure the current page as we know it, which would otherwise
-- led us *2 or /2 away or back in the book.
self.ui.document.no_page_sync = true
end
self:onUpdatePos()
end
end
@ -1538,4 +1663,362 @@ function ReaderRolling:onToggleHideNonlinear()
self:onUpdatePos(true)
end
-- Partial rerendering handling methods and automation.
-- This is a one way path: we start from normal, and on a setting
-- change, we are and stay in a degraded partial rendering mode,
-- that we only exit with reloadDocument(), which makes everything
-- sane by restarting with fresh Reader and crengine instances.
function ReaderRolling:handleRenderingDelayed()
-- Partial rendering will happen on next drawing: let it be known.
-- Pages and ToC will be invalid, some modules may want to disable
-- some features (ie. reading statistics should stop accounting
-- pages read/total pages, as there are invalid, and bogus data
-- would stick in the stats db).
-- We don't send it on each partial rendering (which can happen as
-- we turn pages, only on setting changes (if this would be needed,
-- send this or another event in :handlePartialRerendering()).
local first_partial_rerender = self.rendering_state == nil
self.ui:handleEvent(Event:new("DocumentPartiallyRerendered", first_partial_rerender))
-- Start the automation, ensuring we'll soon be rendering in background
self:setupRerenderingAutomation()
self._stepRerenderingAutomation(self.RENDERING_STATE.PARTIALLY_RERENDERED)
-- Have ReaderView draw the current page, which will provoke the partial rerendering
-- of the DocFragement the current page is from.
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderRolling:handlePartialRerendering()
-- Called by ReaderView after crengine drawing, so we can notice if a partial
-- rerendering did happen or not
local nb_partial_rerenderings = self.ui.document:getPartialRerenderingsCount()
if nb_partial_rerenderings == self.nb_partial_rerenderings then
return -- no partial rerendering
end
logger.dbg("partial rerenderings done", self.nb_partial_rerenderings, ">", nb_partial_rerenderings)
self.nb_partial_rerenderings = nb_partial_rerenderings
self.ui.document:resetCallCache() -- trash invalid cached info
-- crengine should have handled repositionning to the initial page xpointer,
-- and redraw the page if needed.
-- But when tweaking settings multiple times (without changing page), crengine
-- will ensure this for each new displayed page top xpointer, and, the shifts
-- accumulating, we may end up a few pages behind the original page.
-- This is avoided with full rerendering by ensuring _gotoXPointer(self.xpointer)
-- in :updatePos() above. So, do the same if we notice it is needed.
local cur_page = self.ui.document:getCurrentPage()
local cur_xpointer_page = self.ui.document:getPageFromXPointer(self.xpointer)
if cur_page ~= cur_xpointer_page then
logger.dbg("not on the expected page: repositionning and redrawing")
self:_gotoXPointer(self.xpointer)
return true -- ReaderView will repaint
end
-- Current page numbers, total pages and many other have changed.
-- Some may be gathered next time from crengine as we reset the cache,
-- but some are stored in Reader modules (ie. ToC) and won't be updated
-- (some updates may be costly, but mostly, we don't want to hunt them all).
-- At least, be sure everything knows about the current page, so we can
-- turn pages and scroll.
if self.view.view_mode == "page" then
self.ui:handleEvent(Event:new("PageUpdate", self.ui.document:getCurrentPage()))
else
self.current_page = self.ui.document:getCurrentPage()
self.ui:handleEvent(Event:new("PosUpdate", self.ui.document:getCurrentPos(), self.current_page))
end
end
function ReaderRolling:_waitOrKillCurrentRerenderingSubprocess(wait, kill)
-- No need for an asynchronous collector: we'll explicitely call this and wait
-- before going on, even when reloading, to avoid having multiple possibly huge
-- subprocesses at the same time.
-- Returns true if the process is no longer running.
-- We should only return false when wait=false provided (to know if still running).
-- If wait=true, don't return until process is done.
-- If kill=true, kill it, and wait for it to be gone.
if not self._current_rerendering_pid then
return true
end
if ffiutil.isSubProcessDone(self._current_rerendering_pid) then
self._current_rerendering_pid = nil
return true
end
if kill then
wait = true -- we want to be sure it is collected
ffiutil.terminateSubProcess(self._current_rerendering_pid)
end
if wait then
if ffiutil.isSubProcessDone(self._current_rerendering_pid, true) then
self._current_rerendering_pid = nil
return true
end
end
return false
end
function ReaderRolling:setupRerenderingAutomation()
if self._stepRerenderingAutomation then
return
end
-- Once this is created and called, we shouldn't stay long on the current document,
-- we will reload it soon. So, disable standby during the whole steps.
UIManager:preventStandby()
-- Some states will step only when the user is idle
local last_input_event_time = UIManager:getTime() -- (we got here because of some input)
self._watchInputEvent = function()
-- :getTime(), although not accurate, should be good enough, see:
-- https://github.com/koreader/koreader/issues/9087#issuecomment-1419852952
last_input_event_time = UIManager:getTime()
end
UIManager.event_hook:register("InputEvent", self._watchInputEvent)
local next_step_not_before
self._stepRerenderingAutomation = function(next_step)
-- Ensure transitions between partial rerendering steps
logger.dbg("_stepRerenderingAutomation(", next_step, "), currently ", self.rendering_state)
UIManager:unschedule(self._stepRerenderingAutomation)
local reschedule = true
local prev_state = self.rendering_state
if next_step == self.RENDERING_STATE.PARTIALLY_RERENDERED then
-- rendering changed: may be the first, or some setting were changed
-- before we ended all the steps and reload: cancel anything ongoing
self:_waitOrKillCurrentRerenderingSubprocess(true, true)
self.rendering_state = self.RENDERING_STATE.PARTIALLY_RERENDERED
-- Avoid setDirty("ui"), a "partial" has been issued by our caller:
prev_state = self.RENDERING_STATE.PARTIALLY_RERENDERED
next_step_not_before = false
elseif next_step == self.RENDERING_STATE.FULL_RENDERING_IN_BACKGROUND then
self.rendering_state = self.RENDERING_STATE.FULL_RENDERING_IN_BACKGROUND
if self.valid_cache_rendering_hash
and self.valid_cache_rendering_hash == self.ui.document:getDocumentRenderingHash(false) then
-- No need to rerender, the current cache is valid, and we can just reload from it!
-- We'll still show the rendering icon to keep the icon workflow consistent,
-- it will just not display for long
logger.dbg("background rerendering not needed, current cache is usable")
self._current_rerendering_pid = nil -- have this mean no rerendering was needed
else
self._current_rerendering_pid = self:_rerenderInBackground()
end
elseif next_step == self.RENDERING_STATE.FULL_RENDERING_READY then
-- Just show the new icon, work will happen at next schedule
self.rendering_state = self.RENDERING_STATE.FULL_RENDERING_READY
elseif next_step == self.RENDERING_STATE.RELOADING_DOCUMENT then
-- Just show the new icon, work will happen at next schedule
self.rendering_state = self.RENDERING_STATE.RELOADING_DOCUMENT
else -- (from reschedule, next_step=nil)
if self.rendering_state == self.RENDERING_STATE.PARTIALLY_RERENDERED then
-- We want to launch a background rendering, but not before
-- the user has closed all menus, meaning he might be satisfied
-- enough with the partial result; as it may close them to see
-- more of the book; allow for a small delay, assuming that
-- if ReaderUI is still/again at the top, the user is done
-- tweaking settings (if he changes a setting before a reload
-- is triggered, we'll be go at step 1).
-- Let's go with a delay of 3s.
local top_widget = UIManager:getTopmostVisibleWidget() or {}
if top_widget.name == "ReaderUI" then
if not next_step_not_before then -- start counting from now
next_step_not_before = UIManager:getTime() + time.s(3)
else
if UIManager:getTime() >= next_step_not_before then
self._stepRerenderingAutomation(self.RENDERING_STATE.FULL_RENDERING_IN_BACKGROUND)
return
end
-- otherwise, recheck at next schedule
end
end
elseif self.rendering_state == self.RENDERING_STATE.FULL_RENDERING_IN_BACKGROUND then
if self._current_rerendering_pid then
-- Be sure the subprocess is still running
if self:_waitOrKillCurrentRerenderingSubprocess(false) then
-- subprocess died... (self._current_rerendering_pid has then be reset to nil)
-- Step forward, as if no background rendering needed
self._stepRerenderingAutomation(self.RENDERING_STATE.FULL_RENDERING_READY)
return
else -- still running
if shared_state[1] ~= 0 then -- done rendering
self._stepRerenderingAutomation(self.RENDERING_STATE.FULL_RENDERING_READY)
return
end
-- otherwise, recheck at next schedule
end
else -- no background rendering needed, step forward
self._stepRerenderingAutomation(self.RENDERING_STATE.FULL_RENDERING_READY)
return
end
elseif self.rendering_state == self.RENDERING_STATE.FULL_RENDERING_READY
or self.rendering_state == self.RENDERING_STATE.RELOADING_DOCUMENT then
-- We'll have to reload the document: we prefer doing it when the user is
-- idle "reading" (that is, ReaderUI is at top, and no input for some time)
-- to not surprise and interrupt him if he is turning pages, reading a dict
-- lookup result, or busy in some other widget or menu.
-- Let's go with 5s of idle time
local do_reload = false
local top_widget = UIManager:getTopmostVisibleWidget() or {}
if top_widget.name == "ReaderUI" then
do_reload = true
if self.ui.highlight.hold_pos or self.ui.highlight.select_mode then
-- Not if text selection in progress (to not reload while the user
-- is selecting, even if idle)
do_reload = false
elseif UIManager:getTime() < last_input_event_time + time.s(5) then
-- Not idle long enough
do_reload = false
end
end
if self.rendering_state == self.RENDERING_STATE.FULL_RENDERING_READY then
if do_reload then
-- Transition to show the reload icon for at least 1s if the
-- book reloads really fast. We'll redo the above conditions checks
-- in case things happened in the meantime.
self._stepRerenderingAutomation(self.RENDERING_STATE.RELOADING_DOCUMENT)
return
end
-- Otherwise, conditions not yet met
else -- self.RENDERING_STATE.RELOADING_DOCUMENT
if not do_reload then
-- Stuff happened: transition back to previous state
self._stepRerenderingAutomation(self.RENDERING_STATE.FULL_RENDERING_READY)
else
-- reload icon shown, ready to reload
if self._current_rerendering_pid and not self:_waitOrKillCurrentRerenderingSubprocess(false) then
-- Rendering subproces still alive, waiting for our signal to save its cache
-- We need to let the subprocess save the cache, and only then reloadDocument.
-- We need to block the UI so no other setting get changed, and the cache
-- is sane to reload from.
-- Let subprocess know it can save cache
shared_state[2] = 1
-- And wait for it to end (successfully or not)
self:_waitOrKillCurrentRerenderingSubprocess(true)
end
-- Otherwise, no background rerendering needed, or the subprocess died: go on with the reload.
-- We're done with background stuff and icon animations: reallow standby
self.rendering_state = self.RENDERING_STATE.DO_RELOAD_DOCUMENT
self.ui:reloadDocument(nil, true) -- seamless reload (no infomsg, no flash)
end
return
end
end
end
if self.rendering_state ~= prev_state then
logger.dbg("_stepRerenderingAutomation", prev_state, ">", self.rendering_state)
-- Have ReaderView redraw and refresh ReaderFlipping and our state icon, avoiding flashes
UIManager:setDirty(self.view.dialog, "ui", self.view.flipping.dimen)
end
if reschedule then
UIManager:scheduleIn(1, self._stepRerenderingAutomation)
end
end
end
function ReaderRolling:tearDownRerenderingAutomation()
if self._stepRerenderingAutomation then
UIManager:unschedule(self._stepRerenderingAutomation)
self._stepRerenderingAutomation = nil
UIManager:allowStandby()
end
if self._watchInputEvent then
UIManager.event_hook:unregister("InputEvent", self._watchInputEvent)
self._watchInputEvent = nil
end
-- Be sure we don't let any zombie
self:_waitOrKillCurrentRerenderingSubprocess(true, true)
Device:enableCPUCores(1)
end
function ReaderRolling:_rerenderInBackground()
Device:enableCPUCores(2)
-- Set up mmap segment to exchange signals between main and sub processes
shared_state[0] = 0
shared_state[1] = 0
shared_state[2] = 0
-- If the fork failed, self._current_rerendering_pid will be false, and the above
-- automation should do as if no rendering needed, and go on with the reload,
-- provoking a full rerendering on load
local child_pid = ffiutil.runInSubProcess(function(my_pid)
logger.dbg("background rerendering started for hash", self.rendering_hash)
-- Disable top left cre progress bar (not really needed, and may cause
-- a crash on SDL "XInitThreads not called")
self.ui.document:setCallback()
self.ui.document:enablePartialRerendering(false) -- we want a full render
-- Rerender: this will take some time
self.ui.document._document:renderDocument()
-- We could check if we would get "Styles have changed...fully reloading the document may be needed"
-- (which happens when CSS properties "display:" and "white-space:" have changed for some nodes, which
-- is rather rare with our style tweaks) here, and do the reload and rerendering in this same background
-- subprocess, and doing this would hide this whole thing from the user, making the UX seamless.
-- But this would need a lot more memory, as we would then have 2 independant DOM in memory.
-- Ie. with a big book and KOReader taking 120 MB, the subprocess would additionally use:
-- - 60 MB when doing a simple rerendering
-- - 130 MB when doing a full load+render
-- So, let's not, and let the user handle that after the coming up reload.
if false and self.ui.document:isBuiltDomStale() then
logger.info("background cre DOM may not be in sync with styles, doing a full reload")
self.ui.document:invalidateCacheFile()
-- Just doing this seems to work, we can keep the same LVDocView, no need for any
-- tedious cleanup:
self.ui.document._document:loadDocument(self.ui.document.file)
self.ui.document._document:renderDocument()
end
-- Rebuild ToC and PageMap data (these are done when needed when requested by frontend,
-- but doing that now in the background will save that time on the next reload).
self.ui.document._document:updateTocAndPageMap()
-- crengine is quite optimized regarding cache updates writes: it will compute hashes
-- of each block and avoid writing them to disk if not changed from its vision of
-- the existing cache at load time. Because of this, we don't want this background
-- subprocess to update the cache until we are sure we'll reload from it.
-- So, let the main process know we are done rendering and waiting for its
-- signal that we can write the cache.
-- We take some precautions to avoid staying alive if nothing is waiting for us
if shared_state[0] ~= 0 and shared_state[0] ~= my_pid then
-- Parent process is no longer waiting for this child
os.exit(0)
end
shared_state[1] = 1 -- done rendering, let parent know, and wait before saving cache
shared_state[2] = 0 -- we're going to wait for this to become non-0
while true do
ffiutil.usleep(250000)
if shared_state[0] ~= 0 and shared_state[0] ~= my_pid then
logger.info("rerendering subprocess: no longer waited, exiting")
os.exit(0)
end
if ffi.C.getppid() ~= koreader_pid then
-- new parent pid: main process exited/crashed
logger.info("rerendering subprocess: parent gone, exiting")
os.exit(0)
end
if shared_state[2] ~= 0 then
break -- we can go on
end
end
logger.dbg("rerendering subprocess: going on saving cache")
self.ui.document._document:close() -- should save the cache and clean things properly
-- ffiutil.sleep(1)
logger.dbg("rerendering subprocess done")
end)
-- If the fork failed, or in _stepRerenderingAutomation() when we notice the subprocess has died
-- (probably because of out of memory), should we disable partial rendering for this book?
shared_state[0] = child_pid or 0
return child_pid
end
return ReaderRolling

View File

@ -407,10 +407,12 @@ function ReaderThumbnail:_getPageImage(page)
if self.ui.view.highlight.lighten_factor < 0.3 then
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
end
self.ui.highlight.select_mode = false -- Remove any select mode icon
if self.ui.rolling then
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
-- will be croped out after drawing), we will show them just as rendered.
self.ui.rolling.rendering_state = nil -- Remove any partial rerendering icon
self.ui.view:onSetViewMode("page") -- Get out of scroll mode
if self.ui.font.gamma_index < 30 then -- Increase font gamma (if not already increased),
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
@ -503,6 +505,7 @@ end
-- CRE: emitted after a re-rendering
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks

View File

@ -190,6 +190,14 @@ function ReaderView:paintTo(bb, x, y)
elseif self.view_mode == "scroll" then
self:drawScrollView(bb, x, y)
end
local should_repaint = self.ui.rolling:handlePartialRerendering()
if should_repaint then
-- ReaderRolling may have repositionned on another page containing
-- the xpointer of the top of the original page: recalling this is
-- all there is to do.
self:paintTo(bb, x, y)
return
end
end
-- dim last read area
@ -226,7 +234,8 @@ function ReaderView:paintTo(bb, x, y)
self.footer:paintTo(bb, x, y)
end
-- paint flipping or select mode sign
if self.flipping_visible or self.ui.highlight.select_mode then
if self.flipping_visible or self.ui.highlight.select_mode
or (self.ui.rolling and self.ui.rolling.rendering_state) then
self.flipping:paintTo(bb, x, y)
end
for _, m in pairs(self.view_modules) do

View File

@ -63,7 +63,8 @@ local logger = require("logger")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local Input = Device.input
local Screen = Device.screen
local T = ffiUtil.template
local ReaderUI = InputContainer:extend{
@ -109,6 +110,7 @@ function ReaderUI:init()
-- cap screen refresh on pan to 2 refreshes per second
local pan_rate = Screen.low_pan_rate and 2.0 or 30.0
Input:inhibitInput(true) -- Inhibit any past and upcoming input events.
Device:setIgnoreInput(true) -- Avoid ANRs on Android with unprocessed events.
self.postInitCallback = {}
@ -479,6 +481,7 @@ function ReaderUI:init()
self.postReaderCallback = nil
Device:setIgnoreInput(false) -- Allow processing of events (on Android).
Input:inhibitInputUntil(0.2)
-- print("Ordered registered gestures:")
-- for _, tzone in ipairs(self._ordered_touch_zones) do
@ -562,7 +565,7 @@ end
--- @note: Will sanely close existing FileManager/ReaderUI instance for you!
--- This is the *only* safe way to instantiate a new ReaderUI instance!
--- (i.e., don't look at the testsuite, which resorts to all kinds of nasty hacks).
function ReaderUI:showReader(file, provider)
function ReaderUI:showReader(file, provider, seamless)
logger.dbg("show reader ui")
if lfs.attributes(file, "mode") ~= "file" then
@ -584,21 +587,22 @@ function ReaderUI:showReader(file, provider)
UIManager:broadcastEvent(Event:new("ShowingReader"))
provider = provider or DocumentRegistry:getProvider(file)
if provider.provider then
self:showReaderCoroutine(file, provider)
self:showReaderCoroutine(file, provider, seamless)
end
end
function ReaderUI:showReaderCoroutine(file, provider)
function ReaderUI:showReaderCoroutine(file, provider, seamless)
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), BD.filepath(file)),
timeout = 0.0,
invisible = seamless,
})
-- doShowReader might block for a long time, so force repaint here
UIManager:forceRePaint()
UIManager:nextTick(function()
logger.dbg("creating coroutine for showing reader")
local co = coroutine.create(function()
self:doShowReader(file, provider)
self:doShowReader(file, provider, seamless)
end)
local ok, err = coroutine.resume(co)
if err ~= nil or ok == false then
@ -612,7 +616,7 @@ function ReaderUI:showReaderCoroutine(file, provider)
end)
end
function ReaderUI:doShowReader(file, provider)
function ReaderUI:doShowReader(file, provider, seamless)
logger.info("opening file", file)
-- Only keep a single instance running
if ReaderUI.instance then
@ -664,7 +668,7 @@ function ReaderUI:doShowReader(file, provider)
FileManager.instance:onClose()
end
UIManager:show(reader, "full")
UIManager:show(reader, seamless and "ui" or "full")
end
-- NOTE: The instance reference used to be stored in a private module variable, hence the getter method.
@ -832,7 +836,7 @@ function ReaderUI:onReload()
self:reloadDocument()
end
function ReaderUI:reloadDocument(after_close_callback)
function ReaderUI:reloadDocument(after_close_callback, seamless)
local file = self.document.file
local provider = getmetatable(self.document).__index
@ -842,6 +846,7 @@ function ReaderUI:reloadDocument(after_close_callback)
self:handleEvent(Event:new("CloseReaderMenu"))
self:handleEvent(Event:new("CloseConfigMenu"))
self:handleEvent(Event:new("PreserveCurrentSession")) -- don't reset statistics' start_current_period
self.highlight:onClose() -- close highlight dialog if any
self:onClose(false)
if after_close_callback then
@ -849,7 +854,7 @@ function ReaderUI:reloadDocument(after_close_callback)
after_close_callback(file, provider)
end
self:showReader(file, provider)
self:showReader(file, provider, seamless)
end
function ReaderUI:switchDocument(new_file)

View File

@ -322,9 +322,9 @@ function CreDocument:render()
logger.dbg("CreDocument: rendering done.")
end
function CreDocument:getDocumentRenderingHash()
function CreDocument:getDocumentRenderingHash(extended)
if self.been_rendered then
return self._document:getDocumentRenderingHash()
return self._document:getDocumentRenderingHash(extended)
end
return 0
end
@ -1390,6 +1390,26 @@ function CreDocument:setCallback(func)
return self._document:setCallback(func)
end
function CreDocument:canBePartiallyRerendered()
return self._document:canBePartiallyRerendered()
end
function CreDocument:isPartialRerenderingEnabled()
return self._document:isPartialRerenderingEnabled()
end
function CreDocument:enablePartialRerendering(enable)
return self._document:enablePartialRerendering(enable)
end
function CreDocument:getPartialRerenderingsCount()
return self._document:getPartialRerenderingsCount()
end
function CreDocument:isRerenderingDelayed()
return self._document:isRerenderingDelayed()
end
function CreDocument:isBuiltDomStale()
return self._document:isBuiltDomStale()
end
@ -1398,6 +1418,10 @@ function CreDocument:hasCacheFile()
return self._document:hasCacheFile()
end
function CreDocument:isCacheFileStale()
return self._document:isCacheFileStale()
end
function CreDocument:invalidateCacheFile()
self._document:invalidateCacheFile()
end
@ -1794,6 +1818,8 @@ function CreDocument:setupCallCache()
elseif name == "getPageFlow" then no_wrap = true
elseif name == "getPageNumberInFlow" then no_wrap = true
elseif name == "getTotalPagesLeft" then no_wrap = true
elseif name == "getDocumentRenderingHash" then no_wrap = true
elseif name == "getPartialRerenderingsCount" then no_wrap = true
-- Some get* have different results by page/pos
elseif name == "getLinkFromPosition" then cache_by_tag = true

View File

@ -83,6 +83,8 @@ local order = {
"document_save",
"document_end_action",
"language_support",
"----------------------------",
"partial_rerendering",
},
device = {
"keyboard_layout",

View File

@ -62,6 +62,7 @@ local STATISTICS_SQL_BOOK_TOTALS_QUERY = [[
local ReaderStatistics = Widget:extend{
name = "statistics",
start_current_period = 0,
preserved_start_current_period = nil, -- should stay a class property
curr_page = 0,
id_curr_book = nil,
is_enabled = nil,
@ -120,6 +121,10 @@ function ReaderStatistics:init()
}
self.start_current_period = os.time()
if ReaderStatistics.preserved_start_current_period then
self.start_current_period = ReaderStatistics.preserved_start_current_period
ReaderStatistics.preserved_start_current_period = nil
end
self:resetVolatileStats()
self.settings = G_reader_settings:readSetting("statistics", self.default_settings)
@ -246,6 +251,23 @@ function ReaderStatistics:onDocumentRerendered()
self.data.pages = new_pagecount
end
function ReaderStatistics:onDocumentPartiallyRerendered(first_partial_rerender)
if not first_partial_rerender then return end -- already done
-- Override :onPageUpdate() to not account page changes from now on
self.onPageUpdate = function(this, pageno)
if pageno == false then -- happens from onCloseDocument
-- We need to call the original one to get saved previous statistics correct
return ReaderStatistics.onPageUpdate(this, false)
end
return
end
end
function ReaderStatistics:onPreserveCurrentSession()
-- Can be called before ReaderUI:reloadDocument() to not reset the current session
ReaderStatistics.preserved_start_current_period = self.start_current_period
end
function ReaderStatistics:resetVolatileStats(now_ts)
-- Computed by onPageUpdate
self.pageturn_count = 0