Files
UnrealEngineUWP/Engine/Source/Runtime/PakFile/Private/IPlatformFilePak.cpp
Andrew Grant 48331caa30 Copying //UE4/Orion-Staging to //UE4/Main (Source: //Orion/Dev-General @ 3028454)
#lockdown Nick.Penwarden

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3028439 on 2016/06/27 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 28 @ CL 3028090

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3028437 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3027952 on 2016/06/26 by Jurre.deBaare

	- Integrating code fixes/changes from Simplygon for Normals issue on Simplygon Swarm + landscape culling
	- Readded landscape/volume culling for in-engine static mesh merging path
	- Fixed issue with r.HLOD force -1 (now stops forcing hlods)
	- Marked hlodcullingvolume as experimental
	- Added Landscape culling flag + precision level

	#codereview Michael.Noland
	#rb Michael.Noland
	#tests build clusters locally/cloud + landscape culling tests

Change 3027702 on 2016/06/25 by Jason.Bestimt

	#ORION_DG - Merge MAIN @ CL 3027698

	#RB:none
	#Tests:none

Change 3027312 on 2016/06/24 by Daniel.Lamb

	Changed the way reentry data is stored in the cooker, reduce work load by main thread, also fix issue with reentry data being used from incorrect packages.
	#rb Peter.Sauerbrei
	#test QA game launch on and cook by the book + cook on the fly paragon

Change 3027165 on 2016/06/24 by Daniel.Lamb

	Fix compiler warnings from cvars changes.
	#rb none
	#test cook paragon

Change 3026900 on 2016/06/24 by Daniel.Lamb

	Change the way low quality lightmap shaders are enabled / disabled as the engine can't be used in the should cache function.
	#rb Marcus.Wasmer
	#test Cook on the fly paragon
	#codereview Rolando.Caloca

Change 3026874 on 2016/06/24 by Olaf.Piesche

	#jira OR-18363
	fix distortion in particle macro UVs with camera movement

	#rb frank.fella
	#tests PC Editor/Game

Change 3026494 on 2016/06/24 by jason.bestimt

	#ORION_MAIN - Merge 28 @ CL 3026460

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3026476 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	#CodeReview: jason.bestimt

Change 3026381 on 2016/06/24 by Graeme.Thornton

	Strip particle modules, emitters and lodlevels from cooked server data. Saves ~10mb of runtime memory on Paragon

	#rb simon.tovey
	#codereview dmitry.rekman
	#tests pc cooked client/server, golden path

Change 3025760 on 2016/06/23 by jason.bestimt

	#ORION_MAIN - Merge 28 @ CL 3025687

	#RB:none
	#tests:none

	#ROBOMERGE-SOURCE: CL 3025709 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	//Orion/Dev-General/OrionGame/Content/Characters/Heroes/Sword/Meshes/sword.uasset - can't integrate exclusive file already opened
	//Orion/Dev-General/OrionGame/Content/Characters/Heroes/Sword/Meshes/sword_Skeleton.uasset - can't integrate exclusive file already opened
	#CodeReview: jason.bestimt

Change 3025661 on 2016/06/23 by Mieszko.Zielinski

	Added a feature to PathfollowingComponent allowing it to identify when it starts following a navigation link #UE4

	#rb Lukasz.Furman
	#test golden path

Change 3025359 on 2016/06/23 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: sam.zamani
	#online,externalui,ps4
	- expose access to reset cookies before invoking embedded web browser
	- fixed not capturing resulting Url when PS4 browser is closed

	#rb none
	#tests ps4

	#ROBOMERGE-SOURCE: CL 3025356 in //Orion/Release-28/... via CL 3025358
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3025184 on 2016/06/23 by Lina.Halper

	Fix crash with morphtargets

	#jira: OR-24257
	#rb: Rolando.Caloca
	#tests: switching mesh with different morphtargets in editor

Change 3024714 on 2016/06/23 by Lukasz.Furman

	added vlog extension to visual debugger tool, added object whitelist to vlog to include selected minion data in  games started with -LogBotGame param
	#rb Mieszko.Zielinski
	#tests server game with and without LogBotGame cmdline

Change 3024709 on 2016/06/23 by Daniel.Lamb

	Added support for async save when saving seperate bulk data file.
	Added mb saved to cooking package stats.
	#rb Andrew.Grant,Wes.Hunt
	#test Cook Paragon

Change 3024674 on 2016/06/23 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: sam.zamani
	Merging //Orion/Release-28 to Main (//Orion/Main)

	#online,identity,ps4
	- add psplus flag to online account after privilege check

	#rb none
	#tests ps4 login flow

	#ROBOMERGE-SOURCE: CL 3024672 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3024510 on 2016/06/23 by Graeme.Thornton

	Added more info to the dumpparticlesystems exec command output

	#rb simon.tovey
	#tests cooked pc client, golden path

Change 3024504 on 2016/06/23 by Graeme.Thornton

	Asset registry memory optimisation - make sure all dependency node link arrays are sized exactly as they need to be (saves ~1mb)

	#rb robert.manuszewski
	#tests windows cooked client, golden path

Change 3024213 on 2016/06/22 by Ryan.Gerleve

	Handle a rare case in replays where the spectator controller is null but we still find a valid NetGUID for it.
	Added an ensure before a check that would fail in this case so we'll know if it happens again.

	#tests replays
	#rb john.pollard

Change 3024127 on 2016/06/22 by John.Pollard

	Bulk merge using Dev-Networking_->_Dev-General_(Orion)

	3002989
	Add ability to skip missing/changed properties in FFastArraySerializer

	3003072
	Fix crash related to new replay backwards compatibility changes

	3008097
	Renaming CompatibleReplayout to NetFieldExportGroup preparing to unify ability to use FNetFieldExportGroup for both RepLayout and FClassNetCache

	3009684
	Added ability to use FNetFieldExportGroups for FClassNetCache as well as FRepLayout

	* Adds ability to track missing/changed custom delta properties names
	* Adds ability to track missing/changed RPC's

	3013455
	Add ability to skip over RPC parameters that have changes/missing in replays for backwards compatibility

	* We now mark FClassNetCache properties as bIncompatible so we don't spam forever when they are out of date
	* No longer factor in parameters when building checksum for RPC's
	* Save FNetFieldExport handle for FClassNetCache fields
	* Use WriteIntWrapped when saving FClassNetCache fields FNetFieldExport handles (and use NetFieldExportGroup->NetFieldExports.Num() to determine max value)
	* Lots of cleanup and sanity checking improvements

	3018078
	Optimize replay checkpoints

	* Share work that was already done during normal replication, and re-use this data to remove the need to compare any checkpoint properties
	* When saving a checkpoint, we no longer create a new connection and new channels, instead we re-use the existing channels, and added the ability to make this a transient operation
	* To make custom delta serialize properties work, we compare against the CDO state instead of current state when saving out a checkpoint

	3021196
	Fix issue with Fast tarray exporting package map info during checkpoints

	* Add ability to save and restore package map ack status
	* Save package map ack status before saving checkpoint, and then restore it back afterwards, this will then allow the stream that writes afterward to re-export anything that was new in the checkpoint
	* No longer queue up reliable bunches on ack list for internal ack connections

	3024033
	Prune the cached change list before using when saving out checkpoints

	* Fixes issues when saving checkpoint, and the live properties change array size, which throws everything off when it comes time to use the LifetimeChangelist

	3024034
	Don't close channels when saving checkpoints, fixes issue where it creates a bunch with bOpen/bClose (because we're forcing all SendBunches to re-open channels), which throws off the stream

	#rb RyanG
	#tests Replays

Change 3024021 on 2016/06/22 by Dmitry.Rekman

	Fix PS4 build.

	#rb Michael.Noland
	#tests none
	#codereview Michael.Noland, Dan.Youhon, Sammy.James

Change 3023734 on 2016/06/22 by Lukasz.Furman

	added replication for input events and tool state for both gameplay debugger categories and extensions
	#ue4
	#rb Mieszko.Zielinski
	#tests PIE, server game

Change 3023708 on 2016/06/22 by Dmitry.Rekman

	Add a separate macro for a poison malloc proxy usage.

	#rb Michael.Noland
	#codereview Michael.Noland, Gil.Gribb
	#tests Compiled OrionServer-Linux-Debug, ran it

Change 3023670 on 2016/06/22 by Nick.Darnell

	Home screen - Fixing safezones on the homescreen.  Adding a scale option to the XP_Fill widget.  Adding better animations to the tiles.  Showing subtitles again.  Hero XP summary widget now takes you to that hero.

	#rb none
	#tests PIE

Change 3023632 on 2016/06/22 by Dmitry.Rekman

	Fix incorrect matching condition in comments.

	#rb none
	#tests Compiled OrionServer-Linux-Debug
	#codereview Michael.Noland

Change 3023475 on 2016/06/22 by Rolando.Caloca

	O - Back out changelist 3022847 as it broke SSS on PC
	#rb none
	#tests re-run editor on PC

Change 3023178 on 2016/06/22 by Michael.Noland

	Engine: Added system memory and % of time spent hitching to analytics
	#rb bob.tellez
	#tests Tested a match in Paragon

Change 3022963 on 2016/06/22 by Mieszko.Zielinski

	Fixed a subtle navigation repathing bug #UE4

	While repathing to a location, rather than an actor, the navigation query used path's end while it should be using the original query's EndLocation.

	#rb Lukasz.Furman
	#test golden path

Change 3022865 on 2016/06/22 by David.Ratti

	gameplay cue editor: remove "GameplayCue_" prefix from default GameplayCueNotify filename in default ability system projects

	#rb none
	#tests ability system sample project

Change 3022847 on 2016/06/22 by Rolando.Caloca

	O - Remove checkerboard SSS rendering and recombine post process pass when SSS not enabled
	#rb Marcus.Wassmer
	#codereview Marcus.Wassmer, Brian.Karis
	#tests Load Agora_P, check perf, toggle r.SSS.Quality 1/0, check characters with skin

Change 3022804 on 2016/06/22 by Mieszko.Zielinski

	Fixed AIController clearing out CachedGameplayTasksComponent on UnPosses, even if Pawn was not the CachedGameplayTasksComponent's owner #UE4

	Also, made running BT not clearing info in BB if AI's current BB is compatible with the one required by BT

	#rb Lukasz.Furman
	#test golden path

Change 3022674 on 2016/06/22 by Robert.Manuszewski

	Reimplementing CL #2993969 (Dev-Blueprints) by Maciej.Mroz:

	UE-30729 Crash in Native Orion when selecting Sword or Tomahawk

	Clear AsyncLoading in subobjects.

	#jira OR-23997

	#rb me
	#tests Golden path in editor build, selecting Venus

Change 3022405 on 2016/06/21 by Ryan.Gerleve

	Fix for OR-23948, crash with death cam enabled. Use a weak pointer to store the viewer on the DemoNetDriver and update it when the game player controller is received.

	#rb john.pollard
	#tests bug repro

Change 3022387 on 2016/06/21 by Ryan.Gerleve

	Added the ability to disable ticking of individual worlds.

	#tests golden path
	#rb john.pollard
	#codereview marc.audy

Change 3022312 on 2016/06/21 by Nick.Darnell

	Home Screen - Adding a max aspect ratio lock to SBox, may need some more fine tuning math may still be a bit pants in some cases.  The XP ovewview panels now have a basic support for account and hero progression.  Hero one shows the last hero you played, widget is invisibile until you play your first game.  The tiles now use the Max Aspect Ratio to prevent stretching forever on 21:9 monitors causing them to just look crazy.  Weekly quests now show the weekly quests screen when clicked.  OrionUserWidgetBase no longer prevents blueprints from running code in reponse to mouse down/up actions if the userwidget consumes input - it always calls the blueprint code first, and always returns handled if it consumes input.

	#rb none
	#tests PIE

Change 3022207 on 2016/06/21 by Wes.Hunt

	Fix Analytics provider to retain passed in AppVersion string instead of using default %VERSION%.
	#rb none
	#tests run windows server with one bot connecting and checking analytics version is what is expected.

Change 3021808 on 2016/06/21 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: josh.markiewicz
	#UE4 - call GetTotalMicroseconds instead of GetTotalMilliseconds * 1000
	#rb none
	#tests compile run ps4

	#ROBOMERGE-SOURCE: CL 3021805 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3021663 on 2016/06/21 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: josh.markiewicz
	#UE4 - CancelFindSessions() didn't null out search result
	- future FindSession() calls would fail with "search in progress"
	#rb joe.wilcox
	#tests UT matchmaking

	#ROBOMERGE-SOURCE: CL 3021655 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3021508 on 2016/06/21 by Marcus.Wassmer

	Remove anti-ghosting AA for now.
	Causes dithered transparency to be very wrong (Dekker shoulders)
	And also a border around all characters of 'noisy fuzz' even when they are not moving
	#rb none
	#test PC/PS4
	#codereview Brian.Karis,Jordan.Walker

Change 3021475 on 2016/06/21 by Marcus.Wassmer

	Duplicate fix for subsurface spec in prep for PS4 optimization. (DevRendering 3018664)
	unified some code for easier maintainance, fixed missing multiply from former change
	#rb none
	#test PS4/PC agora

Change 3021468 on 2016/06/21 by Michael.Noland

	Physics: Added more information when convex cooking partially succeeds (fails initially but succeeds with inflation) indicating the full path of the mesh that failed
	#rb ori.cohen
	#tests Compiled changes, will provide more information when issue reoccurs in build machine cooks
	#jira OR-24082

Change 3021460 on 2016/06/21 by Michael.Noland

	Engine: Added time spent in each hitch bucket to FPS chart .log output and analytics output
	#rb bob.tellez
	#tests Tested a match in Paragon
	#codereview dmitry.rekman

Change 3021368 on 2016/06/21 by Marcus.Wassmer

	Create Tonemapper configuration with no ColorFringe to save .15ms on PS4 when colorfringe is not used.
	#rb rolando.caloca
	#test agora with/wo new config
	#codereview brian.karis

Change 3021119 on 2016/06/21 by David.Ratti

	Make -notimeouts work during initial connecting phase
	#codereview John.Pollard
	#rb none
	#tests pie agora

Change 3021048 on 2016/06/21 by David.Ratti

	minor tweaks to gameplay cues:
	-Descriptions of engine GC notify classes
	-Added 'auto attach to owner' flag on actor notify class.

	#rb none
	#test ability system sample project

Change 3020694 on 2016/06/20 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3020301

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3020674 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#CodeReview: cody.haskell

Change 3020624 on 2016/06/20 by Michael.Noland

	Engine: Pushing more fps chart analytics up to engine level code from Paragon
	Engine: Pushing benchmark config vars up to engine level code from Paragon, and added records of individual sub-steps of benchmarks
	#rb Bob.Tellez
	#tests Golden path Solo VS AI in Paragon and forced a match
	#codereview bob.tellez, peter.knepley

Change 3020181 on 2016/06/20 by Dmitry.Rekman

	Re-do allowing allocations in NullRHI (OR-24029).

	- Originally CL 2990582 by MarcusW, stomped during merge by CL 3006926.

	#rb none
	#codereview Marcus.Wassmer, Andrew.Grant
	#tests none

Change 3020139 on 2016/06/20 by Ryan.Gerleve

	Added ability to pause replay recording while keeping the current replay open.

	#rb john.pollard
	#tests paused deathcam recording while disabled

Change 3019817 on 2016/06/20 by Dmitry.Rekman

	Poison allocated/freed memory in Debug and Development (non-editor) configs.

	- With this Paragon client may be more likely to crash on start.

	#rb Steve.Robb
	#codereview  Robert.Manuszewski, Michael.Noland, Andrew.Grant, Gil.Gribb, Steve.Robb
	#tests Built Linux server and Windows client, ran them, also built Windows Orion editor.

Change 3019599 on 2016/06/20 by Rolando.Caloca

	O - Fix flickering on heroes with morph targets
	#rb Marcus.Wassmer
	#tests Load Agora_P
	#jira OR-23866

Change 3019581 on 2016/06/20 by Wes.Hunt

	Fix crash reporter client analytics for internal builds. (Pushing critical fix immediately from //UE4/Orion-Staging)
	#rb Chris.Wood
	#tests none

Change 3019524 on 2016/06/20 by David.Ratti

	call APawn::OnRep_Controller when ClientRetryClientRestart sets the pawn directly

	#rb none
	#tests golden path

Change 3019406 on 2016/06/20 by Marcus.Wassmer

	Duplicate 3014956 from Dev-Rendering
	Fixed HLOD and mesh LODs getting hit by Lightmass ray traces that didn't originate from a mesh
	Volume lighting samples and precomputed visibility cells are now only placed on LOD0 (of both mesh LODs and HLOD)
	#rb none
	#test none
	#codereview Jordan.Walker

Change 3019371 on 2016/06/20 by Graeme.Thornton

	Optimize cooked asset registry dependency node data structures. Saves ~10mb on Paragon.

	#rb robert.manuszewski
	#tests tested with cooked pc client + server

Change 3018492 on 2016/06/17 by Laurent.Delayen

	FBoneReferenceCustomization: support editing properties in AnimBP defaults.

	#rb none
	#tests: Sword.

Change 3017974 on 2016/06/17 by Ryan.Gerleve

	Add an option to toggle deathcam in the gameplay settings UI if the OrionRuntimeOption for deathcam is enabled.

	#rb cody.haskell
	#tests settings menu, enabled and disabled deathcam

Change 3017913 on 2016/06/17 by Robert.Manuszewski

	Fixing leaked log archive.

	#rb Steve.Robb
	#tests Cooked Win64 client + server

Change 3017873 on 2016/06/17 by Daniel.Lamb

	Fix warning in diff cooked build.
	#test none
	#rb none

Change 3017676 on 2016/06/17 by Sam.Zamani

	#online,identity,mcp
	fix for uninitialized variable

	#rb dmitry.rekman
	#tests none

Change 3017671 on 2016/06/17 by Robert.Manuszewski

	Fxied and improved log message when cluster assumptions are violated.

	#rb Steve.Robb
	#tests Win64 client + server (cooked) golden path

Change 3017358 on 2016/06/16 by Nick.Atamas

	Checking in Darnell's change that adds correct geometry to widgets inside retainer widgets.

	#rb none
	#test PIE

Change 3017242 on 2016/06/16 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3017179

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3017233 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	//Orion/Dev-General/OrionGame/Content/UI/Tooltips/Hero/AbilityTooltip.uasset - can't integrate exclusive file already opened
	#CodeReview: jason.bestimt

Change 3017237 on 2016/06/16 by Dmitry.Rekman

	Fix accessing uninitialized field (kills valgrind warnings).

	#rb none
	#codereview Michael.Noland, Andrew.Grant, Ori.Cohen
	#tests Compiled and ran Linux server.

Change 3017236 on 2016/06/16 by Dmitry.Rekman

	Initialize missed field (kills valgrind warnings).

	#rb none
	#codereview Michael.Noland, Andrew.Grant
	#tests Compiled and ran Linux server.

Change 3017186 on 2016/06/16 by Dmitry.Rekman

	Linux: Add hooks for libcrypto memory functions.

	- Libcurl uses OpenSSL, which allocates memory using libcrypto's CRYPTO_malloc() and apparently on purpose does not initialize it.
	- This change a) redirects these allocations to use UE's malloc  b) initializes it with zeros, avoiding valgrind's warnings.
	- This behavior is not used on Shipping configurations because the impact on entropy is not understood (TBD later).

	#rb Michael.Noland, Rob.Cannaday (original version)
	#codereview Michael.Noland, Rob.Cannaday, Alex.Fennel, Chris.Babcock, Sam.Zamani
	#tests Compiled Linux server and ran it.

Change 3017037 on 2016/06/16 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: andrew.grant
	Merging content fix for driver crash from Release-27

	#ROBOMERGE-SOURCE: CL 3017036 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3016838 on 2016/06/16 by Alexis.Matte

	#jira UE-31901 fix to export all blueprint component to obj

	#rb uriel.doyon
	#codereview matt.kuhlenschmidt
	#test export a blueprint containing multiple staticmesh component to obj

Change 3016629 on 2016/06/16 by Dmitry.Rekman

	Make Binned default on Linux non-editor builds.

	#codereview Andrew.Grant
	#rb none
	#tests none

Change 3016615 on 2016/06/16 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: andrew.grant
	Temporarily disabling jemalloc for Linux
	#rb #tests none
	[CodeReviewed] Dmitry.Rekman

	#ROBOMERGE-SOURCE: CL 3016612 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3016566 on 2016/06/16 by Michael.Noland

	Engine: Fixed a regression that broke safe zone debugging features in Windows by reintroducing code from CL# 2861030 that was accidentally deleted in a merge
	#codereview andrew.grant
	#rb dan.hertzka
	#tests Tested r.DebugSafeZone.Mode 1 with r.DebugSafeZone.TitleRatio 0.9 in PIE and confirmed that SSafeZone obeyed it and the red overlay appeared

Change 3016521 on 2016/06/16 by Ryan.Gerleve

	Client recoreded replay fixes and optimizations from Dev-Networking (and one from //UE4/Main), for deathcam.

	Includes the following CLs from Dev-Networking:
	2997908
	2998001
	2998832
	2999054
	2999057
	2999749
	3000051
	3001361
	3001365
	3004958
	3009972
	3009973

	And this CL from //UE4/Main:
	3015528

	#tests golden path, replays
	#rb john.pollard

Change 3016503 on 2016/06/16 by Brian.Karis

	Fixed uninitialized variables on particle lights. Fixes hair shading.

	#rb none
	#tests editor

Change 3016429 on 2016/06/16 by Max.Chen

	Sequencer: Fix StartTime when clamping start offset 0. Follow up to CL #3009386.

	#jira UE-29167
	#tests Load up AnnounceMaster and adjust leading edge of animation clips
	#rb Frank.Fella

Change 3016356 on 2016/06/16 by Lina.Halper

	- Fix crash on rampage morphtarget

	Merging using //UE4/Dev-Framework_to_//Orion/Dev-General
	 - this is dupe change from Dev-Framework

	#jira: https://jira.ol.epicgames.net/browse/OR-23194
	#rb: Ori.Cohen
	#tests: editor/pie spawn as rampage's alt skin

Change 3015696 on 2016/06/15 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 27.2 @ CL 3015646

	This re-unifies our build pipeline.  Fingers crossed. (only 6 files are actually different)

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3015672 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3015642 on 2016/06/15 by Mieszko.Zielinski

	Fixes to multiple reasons AI bots were getting stuck #Orion

	#rb Lukasz.Furman
	#test golden path

Change 3015622 on 2016/06/15 by Mieszko.Zielinski

	fixed FMetaNavMeshPath doing navmesh path update without checking nav agent if pathfinding should be postponed #UE4

	#rb Lukasz.Furman
	#test golden path

Change 3015514 on 2016/06/15 by Uriel.Doyon

	Fixed GlobalMipBias not affecting max texture resolution.
	This fix is implemented differently in Dev-Rendering in  CL 301498.
	#jira OR-23511
	#rb marcus.wassmer
	#test played game with different quality settings

Change 3015258 on 2016/06/15 by Lina.Halper

	Fix crash with recursive reference between two assets

	#rb: Ori.Cohen
	#tests: Sword

Change 3014988 on 2016/06/15 by Wes.Hunt

	Fix Cook Analytics to correctly use the Legacy provider since it sends to a local data collector.
	#rb daniel.lamb
	#tests compile Orion

Change 3014962 on 2016/06/15 by Olaf.Piesche

	Replicating CL 3013696 from Dev-Rendering; making quality level spawn rate scale work for GPU emitters.

	#rb simon.tovey
	#tests PC editor game

Change 3014958 on 2016/06/15 by Laurent.Delayen

	Added SkeletalMeshComponent::bIncludeComponentLocationIntoBounds to help in cases where SMU_OnlyTickPoseWhenRendered is set, and an animation pushed the mesh beyond the capsule.
	This ensures that when the capsule is in view, the mesh will remain updated.

	#rb Michael.Noland
	#test Sword ultimate from another player's view.

Change 3014833 on 2016/06/15 by Laurent.Delayen

	Fix for Base Heroes having their locomotion blendspace broken.

	#rb Thomas.Sarkanen
	#codereview Thomas.Sarkanen
	#tests Gadget networked PIE

Change 3014688 on 2016/06/15 by Nick.Darnell

	UMG - Fixing IsHovered on UUserWidgets.  SObjectWidget did not properly call the super for MouseEnter/MouseLeave allowing SWidget's implementation to set and unset the bool.

	#rb none
	#tests PIE

Change 3014325 on 2016/06/15 by Marcus.Wassmer

	Duplicate 3012706:
	Scalability CVAR for ContactShadows
	#rb john.billon
	#test flip cvar in editor.

Change 3014230 on 2016/06/15 by Robert.Manuszewski

	Fix potentially missing log output when serializing text of length equal to the async log writer buffer size.

	#rb Steve.Robb
	#tests Tested in editor build (client + server)

Change 3013913 on 2016/06/14 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Updating from DMM (updated from 27.1MM)

	#RB:none
	#Tests:compiled

	#ROBOMERGE-SOURCE: CL 3013912 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3013437 on 2016/06/14 by Simon.Tovey

	Speculative fix for OR-23158

	Couldnt' repro NANs but did see it reading garbage.

	Reinstated the check that direct accesses for particles in a zero size emitter will return null.
	Altered selection code to reselect when partilce is dead or returns null and to not allow new selection on zero size emitters.

	#tests GoldenPath, No broken fx and no more reading garbage.
	#rb Olaf.Piesche

Change 3013063 on 2016/06/14 by Jason.Bestimt

	#ORION_DG - Unclog robomerge from DMM Merge

	#RB:none
	#tests:none

Change 3012936 on 2016/06/14 by Rob.Cannaday

	Fix for multiple account login not kicking previous logins
	Client was not parsing response from backend.  Client was expecting content-type to be "application/json" (using FString::Equals).  Backend was returning "application/json;charset=UTF-8".  Changed usage from FString::Equals to FString::StartsWith
	#jira FORT-25452
	#rb sam.zamani
	#tests multiple account login, frontend only

	Merge from FN CL 3011647, plus fixing one other location expecting "application/json"

Change 3012696 on 2016/06/14 by Max.Chen

	Sequencer: Select actors for corresponding selected keys or sections.

	Copy from Dev-Sequencer

	#jira UE-30727
	#tests Load up AnnounceMaster and select keyframes
	#rb none

Change 3012691 on 2016/06/14 by Max.Chen

	Sequencer: Fix dragging the leading edge of a skeletal animation section so that it adjusts the start offset of the animation clip.

	Copy from Dev-Sequencer

	#jira UE-29167
	#tests Load up AnnounceMaster and adjust leading edge of animation clips
	#rb Frank.Fella

Change 3012690 on 2016/06/14 by Andrew.Grant

	Removed Linux work-around for memory stomp alignment
	#rb none
	#tests compiled

Change 3012687 on 2016/06/14 by Max.Chen

	Sequencer: Fix lower bound when doing post render tick so that the start of a shot doesn't render with the previous shot's time.

	Copy from Dev-Sequencer

	#rb none
	#tests Load up AnnounceMaster and played through sequence

Change 3012627 on 2016/06/14 by Mieszko.Zielinski

	Added logging of current MoveID to PathfollowingComponent's vlog snapshot #UE4

	#rb none
	#test golden path

Change 3012615 on 2016/06/14 by Mieszko.Zielinski

	Improved fix to BTDecorator_Blackboard's latent tasks aborting #UE4

	#rb Lukasz.Furman
	#test golden path

Change 3012572 on 2016/06/14 by Dmitry.Rekman

	Fix realloc with non-default alignment in jemalloc (OR-23541).

	- Removed obsolete check(), the code was already there since CL 1834526.

	#rb none
	#codereview Andrew.Grant, Robert.Manuszewski
	#tests none

Change 3012481 on 2016/06/14 by David.Ratti

	ability system #include fixups and move orion attribute capture marcros into base engine ability system execution class

	#rb none
	#tests ability system sample project

Change 3012457 on 2016/06/14 by Andrew.Grant

	Un-fix misaligned memory-stomp fix for Linux
	#rb none
	#tests compiled

Change 3012320 on 2016/06/14 by Graeme.Thornton

	Fixes for MemoryAnalyser2 solution
	 - Upgraded to VS 2015
	 - Clean up solution configurations. Only leave "Any CPU"
	 - Switch project to build with "Any CPU" rather than "x64".

	Reimplementation of CL 3012221 from Dev-Core

	#rb robert.manuszewski
	#tests opened the main window form correctly in visual studio

Change 3012316 on 2016/06/14 by Thomas.Sarkanen

	Fix copying non-POD structs in the fast path

	Prevents double-deletions of TArrays etc.

	#jira UE-31394 - Fix problems with non-POD UStructs using the anim BP fast-path
	#tests Played PIE & died as Sword in OrionEntry, Exited PIE.
	#rb Martin.Wilson

Change 3012187 on 2016/06/14 by Graeme.Thornton

	Corrected error message when not specifying linux server device command line correctly in UAT

	#rb Dmitry.Rekman
	#tests Checked error message was useful when wrong command line was specified

Change 3012026 on 2016/06/13 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3011936

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3011996 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	//Orion/Dev-General/oriongame/Content/Characters/Heroes/Hammer/Abilities/Subjugate/FX/P_SubjugateSwirls.uasset - can't integrate exclusive file already opened
	#CodeReview: jason.bestimt

Change 3011969 on 2016/06/13 by Brian.Karis

	Tweaks for hair

Change 3011638 on 2016/06/13 by Andrew.Grant

	Fixed issue where RepLayout could use unaligned memoryfor property construction if allocator did not use a suitable default (fixes crash when running with MemStomp).

	Also added sanity check in UScriptStruct::InitializeStruct that memory is correctly aligned before calling constructor

	#codereview Dave.Ratti, John.Pollard
	#rb none
	#tests Ran with/without memstomp

Change 3011575 on 2016/06/13 by jason.bestimt

	#ORION_MAIN - Merge DUI @ CL 3011414

	NOTE - Card data was altered.  Shame shame shame.  Not authoritative in DUI

	#RB:none
	#Tests:none

	[CodeReviewed]: matt.schembari, kerrington.smith, sammy.james, matt.kuhlenschmidt

	#ROBOMERGE-SOURCE: CL 3011552 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	#CodeReview: jason.bestimt

Change 3011462 on 2016/06/13 by Alexis.Matte

	#jira UE-31901 The outputdevice is adding 2 uninitialize character at the end of any log, this is cause by the terminator logic. The fix is to not add those characters when no terminator should be add.

	#rb nick.darnell
	#codereview Robert.Manuszewski
	#test export a obj file and verify all object are exported in maya or max

Change 3011424 on 2016/06/13 by Martin.Wilson

	Hack out fastpath anim bp code until heap corruption issue can be fixed.

	#rb Laurent.Delayen
	#tests Persona + PIE

Change 3011191 on 2016/06/13 by Mieszko.Zielinski

	Added missing initialization of PathFollowingComponent::CurrentMoveInput #UE4

	#rb none
	#test golden path

Change 3011138 on 2016/06/13 by Mieszko.Zielinski

	Switched bots over from travel mode to sprinting #Orion

	#rb Lukasz.Furman
	#test golden path

Change 3011075 on 2016/06/13 by David.Ratti

	Default GameplayAbility instancing policy to InstancePerExecution

	#rb BenZ
	#tests compile

Change 3011051 on 2016/06/13 by David.Ratti

	Add missing include so GameplayAbilitySet.h can be included on its own.

	#rb none
	#tests compile

Change 3010968 on 2016/06/13 by Mieszko.Zielinski

	Fixed console variables crashing on "" string #UE4

	#rb Lukasz.Furman
	#codereview Martin.Mittring
	#test PIE

Change 3010888 on 2016/06/13 by Alexis.Matte

	#jira OR-23301 Close the OS handle when closing the FAsyncWriter.

	#rb Robert.Manuszewski
	#codereview Robert.Manuszewski
	#test try to export a obj file

Change 3010239 on 2016/06/11 by Michael.Noland

	UMG - Adding back the logic to use the normal WidgetTree when the DesignerWidgetTree is not defined. [duplicated fix from CL# 2998267]

	#jira UE-31570
	#tests Compiled some blueprints
	#rb none

Change 3009870 on 2016/06/10 by Wes.Hunt

	Remove logging of analytics payloads from dedicated servers #jira UE-31858
	#codereview:dmitry.rekman
	#rb none
	#tests All testing was done in Dev-Framework. This change was redone in this branch to get it here faster.

Change 3009599 on 2016/06/10 by Michael.Noland

	Rendering: Corrected a misleading help comment on r.StaticMeshLODDistanceScale, explaining how it affects LOD calculations (it multiplies the effective distance, so larger numbers make transitions happen sooner)
	#tests Verified in the output of the help command
	#rb none
	#rn

Change 3009559 on 2016/06/10 by Marcus.Wassmer

	Fix 11/11/10 SceneColorFormat option, enable AntiGhosting on TemporalAA, ensure TemporalAA output is the correct format for accumulating results.  Set SceneColorFormat to 2 on PS4 and lowspec PC.  All in all saves .3-.5ms on PS4 and improves temporalAA ghosting.
	#rb Brian.Karis
	#test Agora PS4 / PC

Change 3009525 on 2016/06/10 by David.Ratti

	Fix case where ObjectLIbrary would not be able to find classes when searching "/Game" path.

	#rb none
	#tests object libraries in paragon

Change 3009228 on 2016/06/10 by David.Ratti

	remove world check. Can be triggered in editor with PIE

	#rb none
	#tests pie

Change 3009050 on 2016/06/10 by Dmitry.Rekman

	Fix LinuxClient platform not being built on Windows, and not instantiating a target platform instance.

	- Fixes by BenM.

	#rb none
	#codereview Ben.Marsh
	#tests Compiled OrionEditor on Linux.

Change 3008973 on 2016/06/10 by Marcus.Wassmer

	Fix Windualshock on VS2015
	#rb Rolando.Caloca
	#test PS4 controller on VS2015 build

Change 3008970 on 2016/06/10 by David.Ratti

	Fix warning about minimal replication tag count
	-Made bit count a config setting. Bumped to 5
	-Minor optimization to UAbilitySystemGlobals::Get()

	#rb none
	#tests goldne path, pie

Change 3008478 on 2016/06/09 by Jason.Bestimt

	#ORION_DG - Merge MAIN @ CL 3008469

	#RB:none
	#Tests:none

Change 3008416 on 2016/06/09 by Andrew.Grant

	Adding 'config' as an option to set both clientconfig / serverconfig when using BuildCookRun
	#review-3008417 Ben.Marsh, Justin.Sargent
	#rb none
	#tests BuildCookRun with config

Change 3008286 on 2016/06/09 by Dmitry.Rekman

	Add LinuxClient target platform.

	#rb none
	#tests Compile OrionEditor on Linux.
	#codereview Brad.Angelcyk, Ben.Marsh

Change 3007978 on 2016/06/09 by jason.bestimt

	#ORION_MAIN - Merge DUI @ CL 3007507

	#RB:none
	#Tests:none

	[CodeReviewed]: matt.schembari, kerrington.smith

	#ROBOMERGE-SOURCE: CL 3007968 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3007771 on 2016/06/09 by Laurent.Delayen

	Fixed GetIntFromComp breaking with INDEX_NONE. Fixes crash in OrientationWarping node.

	#rb martin.wilson
	#codereview martin.wilson
	#tests Sword Leap.

Change 3007436 on 2016/06/09 by David.Ratti

	change designer facing parameter name
	#rb none
	#test compile

Change 3007408 on 2016/06/09 by David.Ratti

	WaitGameplayEffectBlockedImmunity - ability task for listening to immunity events

	#rb danY
	#tests pie

Change 3007250 on 2016/06/09 by bruce.nesbit

	Banner impact location fix. (OR-23179)

	#rb none

	#tests Game+PIE

Change 3007228 on 2016/06/09 by Ben.Marsh

	BuildGraph: Rename -SkipNodesWithoutTickets parameter to -SkipTargetsWithoutTickets, to reflect that it's filtering the list of targets rather than the full graph.

	#rb none
	#tests none

Change 3007225 on 2016/06/09 by Ben.Marsh

	EC: Set the -TicketSignature=... parameter for all BuildGraph jobs started by EC to the URL of the current job.

Change 3006985 on 2016/06/08 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3006936

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3006978 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	#CodeReview: jason.bestimt

Change 3006926 on 2016/06/08 by Andrew.Grant

	Merging //UE4/Main @ 300872 via //UE4/Orion-Staging
	#rb none
	#tests engine QA, orion qa smoke

Change 3006444 on 2016/06/08 by Ben.Marsh

	BuildGraph: Add mechanism to specify that nodes can only be built once for a given changelist. Nodes can have an associated ticket file, and they are only permitted to build if the ticket is assigned to the current job. Tickets are created if they don't exist, and written with a signature specific to the job specified via the -TicketSignature="..." parameter. By default, builds will fail if a ticket has been already granted to another job, but the -SkipNodesWithoutTickets parameter instructs the graph to skip affected nodes instead.

	#rb none
	#tests none

Change 3006389 on 2016/06/08 by Daniel.Lamb

	Delay the processing of packages which aren't ready till the end of the cook.
	This allows other packages to be processed sooner.
	#rb Peter.Sauerbrei
	#test cook orion.

Change 3006306 on 2016/06/08 by Michael.Noland

	Rendering: Added FreezeRendering to the console autocomplete list
	#rn

Change 3006305 on 2016/06/08 by Michael.Noland

	HLOD: Added a way to control how far down the HLOD hierarchy to allow showing (can be used to limit quality loss and streaming texture memory usage on high scalability settings for example)
	Controlled by the new cvar r.HLOD.MaximumLevel, which can be set to the following values:
	-1: No maximum level (default)
	0: Prevent ever showing a HLOD cluster instead of individual meshes
	1: Allow only the first level of HLOD clusters to be shown
	2+: Allow up to the Nth level of HLOD clusters to be shown

	Note: This does not affect the memory used by the HLOD meshes itself or their always loaded low mip levels, it will only save the memory associated with streaming in the higher mip levels

	HLOD: Allowed r.HLOD console command to be used in Test configurations
	HLOD: Removed some dead code in ALODActor and scene view / scene proxy relating to a different way to force visualization that has no trigger
	HLOD: Fixed an uninitialized memory bug in the static mesh scene proxy HLODcoloration visualization code

	#rn
	#codereview jurre.debaare
	#rb marc.audy
	#tests Tested with various settings in Paragon and tried creating some new clusters in the editor

Change 3006304 on 2016/06/08 by Michael.Noland

	Engine: Changed the code in AActor::IncrementalRegisterComponents to obey bAutoRegister for the root component rather than asserting that it is true. Note: If children components have bAutoRegister=true, they will still pull the root component into the fray and cause it to be registered first
	#rb marc.audy
	#tests Tested with code that registers or unregisters HLOD clusters at varying levels in Paragon

Change 3006041 on 2016/06/08 by Andrew.Grant

	Added buildidoverride to shipping whitelist
	#rb none
	#tests compiled and used param in shipping

Change 3005678 on 2016/06/08 by Ben.Marsh

	Back out changelist 3004395

	#rb none
	#tests none

Change 3005265 on 2016/06/07 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3005120

	#RB: none
	#Tests: none

	#ROBOMERGE-SOURCE: CL 3005250 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	#CodeReview: jason.bestimt

Change 3005081 on 2016/06/07 by Daniel.Lamb

	Reworked the way packages which are renamed on load are added to the cooked package list.
	Cooker now has options for MaxMemoryAllowance as a percentage and also MinFreeMemory (which takes into account used system memory not just total system memory).
	#rb Andrew.Grant, Marcus.Wasmer
	#test cook orion

Change 3004752 on 2016/06/07 by Daniel.Lamb

	Requeue packages to the next package on the list instead of to the end of the list.
	#rb Andrew.Grant
	#test cook orion

Change 3004560 on 2016/06/07 by David.Ratti

	Kill timelines, latent actions, timer when recycling gameplay cues

	#rb danY
	#tests pie

Change 3004559 on 2016/06/07 by David.Ratti

	Object Library:
	-Added bool bIncludeOnlyOnDiskAssets that can be set by owner. Passed on to the AR filter when gathering assets.

	GameplayCue Editor:
	-Fix issue with new notifies not showing up after being created through the GC Editor (until restart).

	-Fix issue for new projects, that don't have gameplayclue tags defined, not being able to add gameplay cue tags through the editor without restarting once.

	#rb none
	#tests editor

Change 3004395 on 2016/06/07 by Ben.Marsh

	BuildGraph: Add a script function to ensure exclusive access to a given resource. The AcquireLockFile() function takes two arguments; a path to a text file, and the name of an owner. The file is created and the owner name written to it if it doesn't already exist, otherwise the contents of it are compared against the given owner string. The operation happens transactionally, and the function returns true if file contains the given owner string on exit. Logical and/or conditions are now also short-circuited, so the result of the AcquireLockFile() function can be used to control derived property definitions within an executing job.

	#rb none
	#tests none

Change 3004164 on 2016/06/07 by David.Ratti

	Ability system: use player controller netmode over avatar actor when possible. Fixes issue if torn off, authority, client side actor tries to activate an ability.

	#rb danY
	#tests multi pie

Change 3003837 on 2016/06/07 by David.Ratti

	Ability system engine work
	-Default to /Game as search path for gameplay cues, if no explicit paths are set in the config.

	#rb none
	#tests ability sample project

Change 3002800 on 2016/06/06 by Marcus.Wassmer

	Fix shader crash in PIE
	#rb none
	#test PIE

Change 3002657 on 2016/06/06 by Dmitry.Rekman

	Do not copy to clipboard on crash if headless or on the wrong thread.

	- Could result in crash handler crashing itself in some circumstances.

	#rb none
	#codereveiw Brad.Angelcyk
	#tests Compiled Linux dedicated server and CrashReportClient.

Change 3002546 on 2016/06/06 by Daniel.Lamb

	Improve cooking performance.
	Allow cooker to save other packages opportunistically if main package is compiling shaders.
	Allow cooker to load more packages if there aren't many packages to save.
	#rb Josh.Adams
	#test cook orion

Change 3002369 on 2016/06/06 by Marcus.Wassmer

	Project setting for optional rendering features to reduce shader compile times.
	# of shaders per material is reduced by ~30-40% depending on material
	#rb Daniel.Wright
	#test Editor with/without all options, cooked ps4.

Change 3002142 on 2016/06/06 by David.Ratti

	Ability system engine level:
	-Fix crash when gameplay cue editor starts if there are no gameplay cue paths specified
	-Fix crash when GameplayCue tag is not specified

	#rb none
	#tests sample ability system project, paragon

Change 3002106 on 2016/06/06 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: sam.zamani
	#orion
	- add support for code tokens which can be shared/redeemed
	- refactor of existing friend founder's pack codes to be displayed in a generic way using code token info
	- added CodeToken.FounderFriendInviteT0 for new paragon tier 0 code which grants access to game without also giving a Founder's pack
	- added CodeToken.FounderFriendInviteT1 to replace existing CodeToken.FriendPC and CodeToken.FriendPS4 Founder's pack codes. Existing codes aliased to the new CodeToken.FounderFriendInviteT1 code
	- No longer differentiation between PC/PS4 friend codes
	- "Share Friend Code" button will now process all available codes that can be issued instead of just 1
	- updated Orion service Mcp call for getUnredeemedCodes() to ignore the code template id and return all available codes. Each returned code will also include the type

	[CodeReviewed]: david.nikdel, jason.bestimt
	#rb david.nikdel
	#tests PC PIE using localhost and profile proxy for granting codes, also existing account with legacy codes

	#ROBOMERGE-SOURCE: CL 3002104 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3001218 on 2016/06/05 by jason.bestimt

	#ORION_MAIN - Merge 27.2 @ CL 3001162

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 3001200 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	#CodeReview: jason.bestimt

Change 2999508 on 2016/06/03 by jason.bestimt

	#ORION_MAIN - Merge 27 @ CL 2999463

	#RB:none
	#Tests:none

	#ROBOMERGE-SOURCE: CL 2999498 in //Orion/Main/...
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#ROBOMERGE-SAYS: Beep boop! I couldn't merge this change. Please do it yourself, human.
	//Orion/Dev-General/OrionGame/Content/Audio/ClassesAndMixes/Classes/Master.uasset - can't integrate exclusive file already opened
	#CodeReview: jason.bestimt

Change 2999465 on 2016/06/03 by Robert.Manuszewski

	Whitelisting more DLLs for injection.

	#rb none
	#tests none

Change 2999455 on 2016/06/03 by Lukasz.Furman

	disabled path invalidation events for minions
	#orion
	#rb Mieszko.Zielinski
	#tests PIE with additional debug logging

Change 2998488 on 2016/06/02 by Michael.Noland

	Engine: Prevent forced drawing of spline components in Test configuration
	#codereview james.golding
	#tests Ran a cooked Test build on a map with splines in it
	#rb david.ratti
	#robomerge: main

Change 2997954 on 2016/06/02 by Jason.Bestimt

	#ROBOMERGE-AUTHOR: jon.lietz
	OR-22425

	no longer try to updat the tag map and modifiers when the gameplay effect that is being removed was not active.

	#RB Dave.Ratti
	#Tests golden path

	#ROBOMERGE-SOURCE: CL 2997940 in //Orion/Release-0.27/... via CL 2997943
	#ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 2997750 on 2016/06/02 by Graeme.Thornton

	Add FPakFile::Check() that attempts to open and read the data out of every file in a pak, to check for corruption
	Added -checkpak option for force a check of every mounted pak file

	#rb robert.manuszewski
	#tests tested against cooked pc client. made sure my data succeeded. made sure corrupted data throws an error.

[CL 3031715 by Andrew Grant in Main branch]
2016-06-28 17:59:42 -04:00

3217 lines
92 KiB
C++

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
#include "PakFilePrivatePCH.h"
#include "IPlatformFilePak.h"
#include "SecureHash.h"
#include "FileManagerGeneric.h"
#include "ModuleManager.h"
#include "IPlatformFileModule.h"
#include "IOBase.h"
#include "BigInt.h"
#include "SignedArchiveReader.h"
#include "PublicKey.inl"
#include "AES.h"
#include "GenericPlatformChunkInstall.h"
#include "AsyncFileHandle.h"
DEFINE_LOG_CATEGORY(LogPakFile);
DEFINE_STAT(STAT_PakFile_Read);
DEFINE_STAT(STAT_PakFile_NumOpenHandles);
FFilenameSecurityDelegate& FPakPlatformFile::GetFilenameSecurityDelegate()
{
static FFilenameSecurityDelegate Delegate;
return Delegate;
}
#if USE_NEW_ASYNC_IO
#define USE_PAK_PRECACHE (1) // you can turn this off to use the async IO stuff without the precache
#else
#define USE_PAK_PRECACHE (0)
#endif
/**
* Precaching
*/
#if USE_PAK_PRECACHE
#include "TaskGraphInterfaces.h"
#define PAK_CACHE_GRANULARITY (64 * 1024)
#define PAK_CACHE_MAX_REQUEST (1024 * 1024)
#define PAK_CACHE_MAX_REQUESTS (8)
#define PAK_CACHE_MAX_PRIORITY_DIFFERENCE_MERGE (AIOP_Normal - AIOP_Precache)
DECLARE_MEMORY_STAT(TEXT("PakCache Total"), STAT_PakCacheMem, STATGROUP_Memory);
static int32 GPakCache_Enable = 1;
static FAutoConsoleVariableRef CVar_Enable(
TEXT("pakcache.Enable"),
GPakCache_Enable,
TEXT("If > 0, then enable the pak cache.")
);
static int32 GPakCache_MaxRequestsToLowerLevel = 2;
static FAutoConsoleVariableRef CVar_MaxRequestsToLowerLevel(
TEXT("pakcache.MaxRequestsToLowerLevel"),
GPakCache_MaxRequestsToLowerLevel,
TEXT("Controls the maximum number of IO requests submitted to the OS filesystem at one time. Limited by PAK_CACHE_MAX_REQUESTS.")
);
static int32 GPakCache_MaxRequestSizeToLowerLevelKB = PAK_CACHE_MAX_REQUEST / 1024;
static FAutoConsoleVariableRef CVar_MaxRequestSizeToLowerLevelKB(
TEXT("pakcache.MaxRequestSizeToLowerLevellKB"),
GPakCache_MaxRequestSizeToLowerLevelKB,
TEXT("Controls the maximum size (in KB) of IO requests submitted to the OS filesystem. Limited by PAK_CACHE_MAX_REQUEST.")
);
static int32 GPakCache_MaxPrecacheMemMB = 0;
static FAutoConsoleVariableRef CVar_MaxPrecacheMemMB(
TEXT("pakcache.MaxPrecacheMemMB"),
GPakCache_MaxPrecacheMemMB,
TEXT("Controls the maximum amount of memory (in MB) that will be used for precaching. Total memory use can be higher because not all requests are precache requests. Setting this to zero means unlimited memory.")
);
class FPakPrecacher;
typedef uint64 FJoinedOffsetAndPakIndex;
static FORCEINLINE uint16 GetRequestPakIndexLow(FJoinedOffsetAndPakIndex Joined)
{
return uint16((Joined >> 48) & 0xffff);
}
static FORCEINLINE int64 GetRequestOffset(FJoinedOffsetAndPakIndex Joined)
{
return int64(Joined & 0xffffffffffffll);
}
static FORCEINLINE FJoinedOffsetAndPakIndex MakeJoinedRequest(uint16 PakIndex, int64 Offset)
{
check(Offset >= 0);
return (FJoinedOffsetAndPakIndex(PakIndex) << 48) | Offset;
}
enum
{
IntervalTreeInvalidIndex = 0
};
typedef uint32 TIntervalTreeIndex; // this is the arg type of TSparseArray::operator[]
static uint32 GNextSalt = 1;
// This is like TSparseArray, only a bit safer and I needed some restrictions on resizing.
template<class TItem>
class TIntervalTreeAllocator
{
TArray<TItem> Items;
TArray<int32> FreeItems; //@todo make this into a linked list through the existing items
uint32 Salt;
uint32 SaltMask;
public:
TIntervalTreeAllocator()
{
check(GNextSalt < 4);
Salt = (GNextSalt++) << 30;
SaltMask = MAX_uint32 << 30;
verify((Alloc() & ~SaltMask) == IntervalTreeInvalidIndex); // we want this to always have element zero so we can figure out an index from a pointer
}
inline TIntervalTreeIndex Alloc()
{
int32 Result;
if (FreeItems.Num())
{
Result = FreeItems.Pop();
}
else
{
Result = Items.Num();
Items.AddUninitialized();
}
new ((void*)&Items[Result]) TItem();
return Result | Salt;;
}
void EnsureNoRealloc(int32 NeededNewNum)
{
if (FreeItems.Num() + Items.GetSlack() < NeededNewNum)
{
Items.Reserve(Items.Num() + NeededNewNum);
}
}
FORCEINLINE TItem& Get(TIntervalTreeIndex InIndex)
{
TIntervalTreeIndex Index = InIndex & ~SaltMask;
check((InIndex & SaltMask) == Salt && Index != IntervalTreeInvalidIndex && Index >= 0 && Index < Items.Num() && !FreeItems.Contains(Index));
return Items[Index];
}
FORCEINLINE void Free(TIntervalTreeIndex InIndex)
{
TIntervalTreeIndex Index = InIndex & ~SaltMask;
check((InIndex & SaltMask) == Salt && Index != IntervalTreeInvalidIndex && Index >= 0 && Index < Items.Num() && !FreeItems.Contains(Index));
Items[Index].~TItem();
FreeItems.Push(Index);
if (FreeItems.Num() + 1 == Items.Num())
{
// get rid everything to restore memory coherence
Items.Empty();
FreeItems.Empty();
verify((Alloc() & ~SaltMask) == IntervalTreeInvalidIndex); // we want this to always have element zero so we can figure out an index from a pointer
}
}
FORCEINLINE void CheckIndex(TIntervalTreeIndex InIndex)
{
TIntervalTreeIndex Index = InIndex & ~SaltMask;
check((InIndex & SaltMask) == Salt && Index != IntervalTreeInvalidIndex && Index >= 0 && Index < Items.Num() && !FreeItems.Contains(Index));
}
};
class FIntervalTreeNode
{
public:
TIntervalTreeIndex LeftChildOrRootOfLeftList;
TIntervalTreeIndex RootOfOnList;
TIntervalTreeIndex RightChildOrRootOfRightList;
FIntervalTreeNode()
: LeftChildOrRootOfLeftList(IntervalTreeInvalidIndex)
, RootOfOnList(IntervalTreeInvalidIndex)
, RightChildOrRootOfRightList(IntervalTreeInvalidIndex)
{
}
~FIntervalTreeNode()
{
check(LeftChildOrRootOfLeftList == IntervalTreeInvalidIndex && RootOfOnList == IntervalTreeInvalidIndex && RightChildOrRootOfRightList == IntervalTreeInvalidIndex); // this routine does not handle recursive destruction
}
};
static TIntervalTreeAllocator<FIntervalTreeNode> GIntervalTreeNodeNodeAllocator;
static FORCEINLINE uint64 HighBit(uint64 x)
{
return x & (1ull << 63);
}
static FORCEINLINE bool IntervalsIntersect(uint64 Min1, uint64 Max1, uint64 Min2, uint64 Max2)
{
return !(Max2 < Min1 || Max1 < Min2);
}
template<typename TItem>
// this routine assume that the pointers remain valid even though we are reallocating
static void AddToIntervalTree_Dangerous(
TIntervalTreeIndex* RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
TIntervalTreeIndex Index,
uint64 MinInterval,
uint64 MaxInterval,
uint32 CurrentShift,
uint32 MaxShift
)
{
while (true)
{
if (*RootNode == IntervalTreeInvalidIndex)
{
*RootNode = GIntervalTreeNodeNodeAllocator.Alloc();
}
int64 MinShifted = HighBit(MinInterval << CurrentShift);
int64 MaxShifted = HighBit(MaxInterval << CurrentShift);
FIntervalTreeNode& Root = GIntervalTreeNodeNodeAllocator.Get(*RootNode);
if (MinShifted == MaxShifted && CurrentShift < MaxShift)
{
CurrentShift++;
RootNode = (!MinShifted) ? &Root.LeftChildOrRootOfLeftList : &Root.RightChildOrRootOfRightList;
}
else
{
TItem& Item = Allocator.Get(Index);
if (MinShifted != MaxShifted) // crosses middle
{
Item.Next = Root.RootOfOnList;
Root.RootOfOnList = Index;
}
else // we are at the leaf
{
if (!MinShifted)
{
Item.Next = Root.LeftChildOrRootOfLeftList;
Root.LeftChildOrRootOfLeftList = Index;
}
else
{
Item.Next = Root.RightChildOrRootOfRightList;
Root.RightChildOrRootOfRightList = Index;
}
}
return;
}
}
}
template<typename TItem>
static void AddToIntervalTree(
TIntervalTreeIndex* RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
TIntervalTreeIndex Index,
uint32 StartShift,
uint32 MaxShift
)
{
GIntervalTreeNodeNodeAllocator.EnsureNoRealloc(1 + MaxShift - StartShift);
TItem& Item = Allocator.Get(Index);
check(Item.Next == IntervalTreeInvalidIndex);
uint64 MinInterval = GetRequestOffset(Item.OffsetAndPakIndex);
uint64 MaxInterval = MinInterval + Item.Size - 1;
AddToIntervalTree_Dangerous(RootNode, Allocator, Index, MinInterval, MaxInterval, StartShift, MaxShift);
}
template<typename TItem>
static FORCEINLINE bool ScanNodeListForRemoval(
TIntervalTreeIndex* Iter,
TIntervalTreeAllocator<TItem>& Allocator,
TIntervalTreeIndex Index,
uint64 MinInterval,
uint64 MaxInterval
)
{
while (*Iter != IntervalTreeInvalidIndex)
{
TItem& Item = Allocator.Get(*Iter);
if (*Iter == Index)
{
*Iter = Item.Next;
Item.Next = IntervalTreeInvalidIndex;
return true;
}
Iter = &Item.Next;
}
return false;
}
template<typename TItem>
static bool RemoveFromIntervalTree(
TIntervalTreeIndex* RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
TIntervalTreeIndex Index,
uint64 MinInterval,
uint64 MaxInterval,
uint32 CurrentShift,
uint32 MaxShift
)
{
bool bResult = false;
if (*RootNode != IntervalTreeInvalidIndex)
{
int64 MinShifted = HighBit(MinInterval << CurrentShift);
int64 MaxShifted = HighBit(MaxInterval << CurrentShift);
FIntervalTreeNode& Root = GIntervalTreeNodeNodeAllocator.Get(*RootNode);
if (!MinShifted && !MaxShifted)
{
if (CurrentShift == MaxShift)
{
bResult = ScanNodeListForRemoval(&Root.LeftChildOrRootOfLeftList, Allocator, Index, MinInterval, MaxInterval);
}
else
{
bResult = RemoveFromIntervalTree(&Root.LeftChildOrRootOfLeftList, Allocator, Index, MinInterval, MaxInterval, CurrentShift + 1, MaxShift);
}
}
else if (!MinShifted && MaxShifted)
{
bResult = ScanNodeListForRemoval(&Root.RootOfOnList, Allocator, Index, MinInterval, MaxInterval);
}
else
{
if (CurrentShift == MaxShift)
{
bResult = ScanNodeListForRemoval(&Root.RightChildOrRootOfRightList, Allocator, Index, MinInterval, MaxInterval);
}
else
{
bResult = RemoveFromIntervalTree(&Root.RightChildOrRootOfRightList, Allocator, Index, MinInterval, MaxInterval, CurrentShift + 1, MaxShift);
}
}
if (bResult)
{
if (Root.LeftChildOrRootOfLeftList == IntervalTreeInvalidIndex && Root.RootOfOnList == IntervalTreeInvalidIndex && Root.RightChildOrRootOfRightList == IntervalTreeInvalidIndex)
{
check(&Root == &GIntervalTreeNodeNodeAllocator.Get(*RootNode));
GIntervalTreeNodeNodeAllocator.Free(*RootNode);
*RootNode = IntervalTreeInvalidIndex;
}
}
}
return bResult;
}
template<typename TItem>
static bool RemoveFromIntervalTree(
TIntervalTreeIndex* RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
TIntervalTreeIndex Index,
uint32 StartShift,
uint32 MaxShift
)
{
TItem& Item = Allocator.Get(Index);
uint64 MinInterval = GetRequestOffset(Item.OffsetAndPakIndex);
uint64 MaxInterval = MinInterval + Item.Size - 1;
return RemoveFromIntervalTree(RootNode, Allocator, Index, MinInterval, MaxInterval, StartShift, MaxShift);
}
template<typename TItem>
static FORCEINLINE void ScanNodeListForRemovalFunc(
TIntervalTreeIndex* Iter,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
while (*Iter != IntervalTreeInvalidIndex)
{
TItem& Item = Allocator.Get(*Iter);
uint64 Offset = uint64(GetRequestOffset(Item.OffsetAndPakIndex));
uint64 LastByte = Offset + uint64(Item.Size) - 1;
// save the value and then clear it.
TIntervalTreeIndex NextIndex = Item.Next;
if (IntervalsIntersect(MinInterval, MaxInterval, Offset, LastByte) && Func(*Iter))
{
*Iter = NextIndex; // this may have already be deleted, so cannot rely on the memory block
}
else
{
Iter = &Item.Next;
}
}
}
template<typename TItem>
static void MaybeRemoveOverlappingNodesInIntervalTree(
TIntervalTreeIndex* RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
uint64 MinNode,
uint64 MaxNode,
uint32 CurrentShift,
uint32 MaxShift,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
if (*RootNode != IntervalTreeInvalidIndex)
{
int64 MinShifted = HighBit(MinInterval << CurrentShift);
int64 MaxShifted = HighBit(MaxInterval << CurrentShift);
FIntervalTreeNode& Root = GIntervalTreeNodeNodeAllocator.Get(*RootNode);
uint64 Center = (MinNode + MaxNode + 1) >> 1;
//UE_LOG(LogTemp, Warning, TEXT("Exploring Node %X [%d, %d] %d%d interval %llX %llX node interval %llX %llX center %llX "), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted, MinInterval, MaxInterval, MinNode, MaxNode, Center);
if (!MinShifted)
{
if (CurrentShift == MaxShift)
{
//UE_LOG(LogTemp, Warning, TEXT("LeftBottom %X [%d, %d] %d%d"), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted);
ScanNodeListForRemovalFunc(&Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, MaxInterval, Func);
}
else
{
//UE_LOG(LogTemp, Warning, TEXT("LeftRecur %X [%d, %d] %d%d"), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted);
MaybeRemoveOverlappingNodesInIntervalTree(&Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, FMath::Min(MaxInterval, Center - 1), MinNode, Center - 1, CurrentShift + 1, MaxShift, Func);
}
}
//UE_LOG(LogTemp, Warning, TEXT("Center %X [%d, %d] %d%d"), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted);
ScanNodeListForRemovalFunc(&Root.RootOfOnList, Allocator, MinInterval, MaxInterval, Func);
if (MaxShifted)
{
if (CurrentShift == MaxShift)
{
//UE_LOG(LogTemp, Warning, TEXT("RightBottom %X [%d, %d] %d%d"), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted);
ScanNodeListForRemovalFunc(&Root.RightChildOrRootOfRightList, Allocator, MinInterval, MaxInterval, Func);
}
else
{
//UE_LOG(LogTemp, Warning, TEXT("RightRecur %X [%d, %d] %d%d"), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted);
MaybeRemoveOverlappingNodesInIntervalTree(&Root.RightChildOrRootOfRightList, Allocator, FMath::Max(MinInterval, Center), MaxInterval, Center, MaxNode, CurrentShift + 1, MaxShift, Func);
}
}
//UE_LOG(LogTemp, Warning, TEXT("Done Exploring Node %X [%d, %d] %d%d interval %llX %llX node interval %llX %llX center %llX "), *RootNode, CurrentShift, MaxShift, !!MinShifted, !!MaxShifted, MinInterval, MaxInterval, MinNode, MaxNode, Center);
if (Root.LeftChildOrRootOfLeftList == IntervalTreeInvalidIndex && Root.RootOfOnList == IntervalTreeInvalidIndex && Root.RightChildOrRootOfRightList == IntervalTreeInvalidIndex)
{
check(&Root == &GIntervalTreeNodeNodeAllocator.Get(*RootNode));
GIntervalTreeNodeNodeAllocator.Free(*RootNode);
*RootNode = IntervalTreeInvalidIndex;
}
}
}
template<typename TItem>
static FORCEINLINE bool ScanNodeList(
TIntervalTreeIndex Iter,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
while (Iter != IntervalTreeInvalidIndex)
{
TItem& Item = Allocator.Get(Iter);
uint64 Offset = uint64(GetRequestOffset(Item.OffsetAndPakIndex));
uint64 LastByte = Offset + uint64(Item.Size) - 1;
if (IntervalsIntersect(MinInterval, MaxInterval, Offset, LastByte))
{
if (!Func(Iter))
{
return false;
}
}
Iter = Item.Next;
}
return true;
}
template<typename TItem>
static bool OverlappingNodesInIntervalTree(
TIntervalTreeIndex RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
uint64 MinNode,
uint64 MaxNode,
uint32 CurrentShift,
uint32 MaxShift,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
if (RootNode != IntervalTreeInvalidIndex)
{
int64 MinShifted = HighBit(MinInterval << CurrentShift);
int64 MaxShifted = HighBit(MaxInterval << CurrentShift);
FIntervalTreeNode& Root = GIntervalTreeNodeNodeAllocator.Get(RootNode);
uint64 Center = (MinNode + MaxNode + 1) >> 1;
if (!MinShifted)
{
if (CurrentShift == MaxShift)
{
if (!ScanNodeList(Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
}
else
{
if (!OverlappingNodesInIntervalTree(Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, FMath::Min(MaxInterval, Center - 1), MinNode, Center - 1, CurrentShift + 1, MaxShift, Func))
{
return false;
}
}
}
if (!ScanNodeList(Root.RootOfOnList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
if (MaxShifted)
{
if (CurrentShift == MaxShift)
{
if (!ScanNodeList(Root.RightChildOrRootOfRightList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
}
else
{
if (!OverlappingNodesInIntervalTree(Root.RightChildOrRootOfRightList, Allocator, FMath::Max(MinInterval, Center), MaxInterval, Center, MaxNode, CurrentShift + 1, MaxShift, Func))
{
return false;
}
}
}
}
return true;
}
template<typename TItem>
static bool ScanNodeListWithShrinkingInterval(
TIntervalTreeIndex Iter,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64& MaxInterval,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
while (Iter != IntervalTreeInvalidIndex)
{
TItem& Item = Allocator.Get(Iter);
uint64 Offset = uint64(GetRequestOffset(Item.OffsetAndPakIndex));
uint64 LastByte = Offset + uint64(Item.Size) - 1;
//UE_LOG(LogTemp, Warning, TEXT("Test Overlap %llu %llu %llu %llu"), MinInterval, MaxInterval, Offset, LastByte);
if (IntervalsIntersect(MinInterval, MaxInterval, Offset, LastByte))
{
//UE_LOG(LogTemp, Warning, TEXT("Overlap %llu %llu %llu %llu"), MinInterval, MaxInterval, Offset, LastByte);
if (!Func(Iter))
{
return false;
}
}
Iter = Item.Next;
}
return true;
}
template<typename TItem>
static bool OverlappingNodesInIntervalTreeWithShrinkingInterval(
TIntervalTreeIndex RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64& MaxInterval,
uint64 MinNode,
uint64 MaxNode,
uint32 CurrentShift,
uint32 MaxShift,
TFunctionRef<bool(TIntervalTreeIndex)> Func
)
{
if (RootNode != IntervalTreeInvalidIndex)
{
int64 MinShifted = HighBit(MinInterval << CurrentShift);
int64 MaxShifted = HighBit(FMath::Min(MaxInterval, MaxNode) << CurrentShift); // since MaxInterval is changing, we cannot clamp it during recursion.
FIntervalTreeNode& Root = GIntervalTreeNodeNodeAllocator.Get(RootNode);
uint64 Center = (MinNode + MaxNode + 1) >> 1;
if (!MinShifted)
{
if (CurrentShift == MaxShift)
{
if (!ScanNodeListWithShrinkingInterval(Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
}
else
{
if (!OverlappingNodesInIntervalTreeWithShrinkingInterval(Root.LeftChildOrRootOfLeftList, Allocator, MinInterval, MaxInterval, MinNode, Center - 1, CurrentShift + 1, MaxShift, Func)) // since MaxInterval is changing, we cannot clamp it during recursion.
{
return false;
}
}
}
if (!ScanNodeListWithShrinkingInterval(Root.RootOfOnList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
MaxShifted = HighBit(FMath::Min(MaxInterval, MaxNode) << CurrentShift); // since MaxInterval is changing, we cannot clamp it during recursion.
if (MaxShifted)
{
if (CurrentShift == MaxShift)
{
if (!ScanNodeListWithShrinkingInterval(Root.RightChildOrRootOfRightList, Allocator, MinInterval, MaxInterval, Func))
{
return false;
}
}
else
{
if (!OverlappingNodesInIntervalTreeWithShrinkingInterval(Root.RightChildOrRootOfRightList, Allocator, FMath::Max(MinInterval, Center), MaxInterval, Center, MaxNode, CurrentShift + 1, MaxShift, Func))
{
return false;
}
}
}
}
return true;
}
template<typename TItem>
static void MaskInterval(
TIntervalTreeIndex Index,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
uint32 BytesToBitsShift,
uint64* Bits
)
{
TItem& Item = Allocator.Get(Index);
uint64 Offset = uint64(GetRequestOffset(Item.OffsetAndPakIndex));
uint64 LastByte = Offset + uint64(Item.Size) - 1;
uint64 InterMinInterval = FMath::Max(MinInterval, Offset);
uint64 InterMaxInterval = FMath::Min(MaxInterval, LastByte);
if (InterMinInterval <= InterMaxInterval)
{
uint32 FirstBit = uint32((InterMinInterval - MinInterval) >> BytesToBitsShift);
uint32 LastBit = uint32((InterMaxInterval - MinInterval) >> BytesToBitsShift);
uint32 FirstQWord = FirstBit >> 6;
uint32 LastQWord = LastBit >> 6;
uint32 FirstBitQWord = FirstBit & 63;
uint32 LastBitQWord = LastBit & 63;
if (FirstQWord == LastQWord)
{
Bits[FirstQWord] |= ((MAX_uint64 << FirstBitQWord) & (MAX_uint64 >> (63 - LastBitQWord)));
}
else
{
Bits[FirstQWord] |= (MAX_uint64 << FirstBitQWord);
for (uint32 QWordIndex = FirstQWord + 1; QWordIndex < LastQWord; QWordIndex++)
{
Bits[QWordIndex] = MAX_uint64;
}
Bits[LastQWord] |= (MAX_uint64 >> (63 - LastBitQWord));
}
}
}
template<typename TItem>
static void OverlappingNodesInIntervalTreeMask(
TIntervalTreeIndex RootNode,
TIntervalTreeAllocator<TItem>& Allocator,
uint64 MinInterval,
uint64 MaxInterval,
uint64 MinNode,
uint64 MaxNode,
uint32 CurrentShift,
uint32 MaxShift,
uint32 BytesToBitsShift,
uint64* Bits
)
{
OverlappingNodesInIntervalTree(
RootNode,
Allocator,
MinInterval,
MaxInterval,
MinNode,
MaxNode,
CurrentShift,
MaxShift,
[&Allocator, MinInterval, MaxInterval, BytesToBitsShift, Bits](TIntervalTreeIndex Index) -> bool
{
MaskInterval(Index, Allocator, MinInterval, MaxInterval, BytesToBitsShift, Bits);
return true;
}
);
}
class IPakRequestor
{
friend class FPakPrecacher;
FJoinedOffsetAndPakIndex OffsetAndPakIndex; // this is used for searching and filled in when you make the request
uint64 UniqueID;
TIntervalTreeIndex InRequestIndex;
public:
IPakRequestor()
: OffsetAndPakIndex(MAX_uint64) // invalid value
, UniqueID(0)
, InRequestIndex(IntervalTreeInvalidIndex)
{
}
virtual ~IPakRequestor()
{
}
virtual void RequestIsComplete()
{
}
};
static FPakPrecacher* PakPrecacherSingleton = nullptr;
class FPakPrecacher
{
enum class EInRequestStatus
{
Complete,
Waiting,
InFlight,
Num
};
enum class EBlockStatus
{
InFlight,
Complete,
Num
};
IPlatformFile* LowerLevel;
FCriticalSection CachedFilesScopeLock;
FJoinedOffsetAndPakIndex LastReadRequest;
uint64 NextUniqueID;
int64 BlockMemory;
int64 BlockMemoryHighWater;
struct FCacheBlock
{
FJoinedOffsetAndPakIndex OffsetAndPakIndex;
int64 Size;
uint8 *Memory;
uint32 InRequestRefCount;
TIntervalTreeIndex Index;
TIntervalTreeIndex Next;
EBlockStatus Status;
FCacheBlock()
: OffsetAndPakIndex(0)
, Size(0)
, Memory(nullptr)
, InRequestRefCount(0)
, Index(IntervalTreeInvalidIndex)
, Next(IntervalTreeInvalidIndex)
, Status(EBlockStatus::InFlight)
{
}
};
struct FPakInRequest
{
FJoinedOffsetAndPakIndex OffsetAndPakIndex;
int64 Size;
IPakRequestor* Owner;
uint64 UniqueID;
TIntervalTreeIndex Index;
TIntervalTreeIndex Next;
EAsyncIOPriority Priority;
EInRequestStatus Status;
FPakInRequest()
: OffsetAndPakIndex(0)
, Size(0)
, Owner(nullptr)
, UniqueID(0)
, Index(IntervalTreeInvalidIndex)
, Next(IntervalTreeInvalidIndex)
, Priority(AIOP_MIN)
, Status(EInRequestStatus::Waiting)
{
}
};
struct FPakData
{
IAsyncReadFileHandle* Handle;
int64 TotalSize;
uint64 MaxNode;
uint32 StartShift;
uint32 MaxShift;
uint32 BytesToBitsShift;
FName Name;
TIntervalTreeIndex InRequests[AIOP_NUM][(int32)EInRequestStatus::Num];
TIntervalTreeIndex CacheBlocks[(int32)EBlockStatus::Num];
FPakData(IAsyncReadFileHandle* InHandle, FName InName, int64 InTotalSize)
: Handle(InHandle)
, TotalSize(InTotalSize)
, StartShift(0)
, MaxShift(0)
, BytesToBitsShift(0)
, Name(InName)
{
check(Handle && TotalSize > 0 && Name != NAME_None);
for (int32 Index = 0; Index < AIOP_NUM; Index++)
{
for (int32 IndexInner = 0; IndexInner < (int32)EInRequestStatus::Num; IndexInner++)
{
InRequests[Index][IndexInner] = IntervalTreeInvalidIndex;
}
}
for (int32 IndexInner = 0; IndexInner < (int32)EBlockStatus::Num; IndexInner++)
{
CacheBlocks[IndexInner] = IntervalTreeInvalidIndex;
}
uint64 StartingLastByte = FMath::Max((uint64)TotalSize, (uint64)PAK_CACHE_GRANULARITY);
StartingLastByte--;
{
uint64 LastByte = StartingLastByte;
while (!HighBit(LastByte))
{
LastByte <<= 1;
StartShift++;
}
}
{
uint64 LastByte = StartingLastByte;
uint64 Block = (uint64)PAK_CACHE_GRANULARITY;
while (Block)
{
Block >>= 1;
LastByte >>= 1;
BytesToBitsShift++;
}
BytesToBitsShift--;
check(1 << BytesToBitsShift == PAK_CACHE_GRANULARITY);
MaxShift = StartShift;
while (LastByte)
{
LastByte >>= 1;
MaxShift++;
}
MaxNode = MAX_uint64 >> StartShift;
check(MaxNode >= StartingLastByte && (MaxNode >> 1) < StartingLastByte);
//UE_LOG(LogTemp, Warning, TEXT("Test %d %llX %llX "), MaxShift, (uint64(PAK_CACHE_GRANULARITY) << (MaxShift + 1)), (uint64(PAK_CACHE_GRANULARITY) << MaxShift));
check(MaxShift && (uint64(PAK_CACHE_GRANULARITY) << (MaxShift + 1)) == 0 && (uint64(PAK_CACHE_GRANULARITY) << MaxShift) != 0);
}
}
};
TMap<FName, uint16> CachedPaks;
TArray<FPakData> CachedPakData;
TIntervalTreeAllocator<FPakInRequest> InRequestAllocator;
TIntervalTreeAllocator<FCacheBlock> CacheBlockAllocator;
TMap<uint64, TIntervalTreeIndex> OutstandingRequests;
struct FRequestToLower
{
IAsyncReadRequest* RequestHandle;
TIntervalTreeIndex BlockIndex;
FRequestToLower()
: RequestHandle(nullptr)
, BlockIndex(IntervalTreeInvalidIndex)
{
}
};
FRequestToLower RequestsToLower[PAK_CACHE_MAX_REQUESTS];
TArray<IAsyncReadRequest*> RequestsToDelete;
int32 NotifyRecursion;
FAsyncFileCallBack CallbackFromLower;
uint32 Loads;
uint32 Frees;
uint64 LoadSize;
public:
static void Init(IPlatformFile* InLowerLevel)
{
if (!PakPrecacherSingleton)
{
verify(!FPlatformAtomics::InterlockedCompareExchangePointer((void**)&PakPrecacherSingleton, new FPakPrecacher(InLowerLevel), NULL));
}
check(PakPrecacherSingleton);
}
static FPakPrecacher& Get()
{
check(PakPrecacherSingleton);
return *PakPrecacherSingleton;
}
FPakPrecacher(IPlatformFile* InLowerLevel)
: LowerLevel(InLowerLevel)
, LastReadRequest(0)
, NextUniqueID(1)
, BlockMemory(0)
, BlockMemoryHighWater(0)
, NotifyRecursion(0)
, Loads(0)
, Frees(0)
, LoadSize(0)
{
check(LowerLevel);
if (FPlatformProcess::SupportsMultithreading())
{
GPakCache_MaxRequestsToLowerLevel = FMath::Max(FMath::Min(FPlatformMisc::NumberOfIOWorkerThreadsToSpawn(), GPakCache_MaxRequestsToLowerLevel), 1);
}
else
{
GPakCache_MaxRequestsToLowerLevel = 0;
}
check(GPakCache_MaxRequestsToLowerLevel <= PAK_CACHE_MAX_REQUESTS);
CallbackFromLower =
[this](bool bWasCanceled, IAsyncReadRequest* Request)
{
NewRequestsToLowerComplete(bWasCanceled, Request);
};
}
IPlatformFile* GetLowerLevelHandle()
{
check(LowerLevel);
return LowerLevel;
}
int64 MaxMemPrecache()
{
return GPakCache_MaxPrecacheMemMB * 1024ll * 1024ll;
}
bool HasEnoughRoomForPrecache()
{
return GPakCache_MaxPrecacheMemMB == 0 || BlockMemory < MaxMemPrecache();
}
private: // below here we assume CachedFilesScopeLock until we get to the next section
uint16 GetRequestPakIndex(FJoinedOffsetAndPakIndex OffsetAndPakIndex)
{
uint16 Result = GetRequestPakIndexLow(OffsetAndPakIndex);
check(Result < CachedPakData.Num());
return Result;
}
FJoinedOffsetAndPakIndex FirstUnfilledBlockForRequest(TIntervalTreeIndex NewIndex, FJoinedOffsetAndPakIndex ReadHead = 0)
{
// CachedFilesScopeLock is locked
FPakInRequest& Request = InRequestAllocator.Get(NewIndex);
uint16 PakIndex = GetRequestPakIndex(Request.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(Request.OffsetAndPakIndex);
int64 Size = Request.Size;
FPakData& Pak = CachedPakData[PakIndex];
check(Offset + Request.Size <= Pak.TotalSize && Size > 0 && Request.Priority >= AIOP_MIN && Request.Priority <= AIOP_MAX && Request.Status != EInRequestStatus::Complete && Request.Owner);
if (PakIndex != GetRequestPakIndex(ReadHead))
{
// this is in a different pak, so we ignore the read head position
ReadHead = 0;
}
if (ReadHead)
{
// trim to the right of the read head
int64 Trim = FMath::Max(Offset, GetRequestOffset(ReadHead)) - Offset;
Offset += Trim;
Size -= Trim;
}
static TArray<uint64> InFlightOrDone;
int64 FirstByte = AlignDown(Offset, PAK_CACHE_GRANULARITY);
int64 LastByte = Align(Offset + Size, PAK_CACHE_GRANULARITY) - 1;
uint32 NumBits = (PAK_CACHE_GRANULARITY + LastByte - FirstByte) / PAK_CACHE_GRANULARITY;
uint32 NumQWords = (NumBits + 63) >> 6;
InFlightOrDone.Reset();
InFlightOrDone.AddZeroed(NumQWords);
if (NumBits != NumQWords * 64)
{
uint32 Extras = NumQWords * 64 - NumBits;
InFlightOrDone[NumQWords - 1] = (MAX_uint64 << (64 - Extras));
}
if (Pak.CacheBlocks[(int32)EBlockStatus::Complete] != IntervalTreeInvalidIndex)
{
OverlappingNodesInIntervalTreeMask<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
Pak.BytesToBitsShift,
&InFlightOrDone[0]
);
}
if (Request.Status == EInRequestStatus::Waiting && Pak.CacheBlocks[(int32)EBlockStatus::InFlight] != IntervalTreeInvalidIndex)
{
OverlappingNodesInIntervalTreeMask<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
Pak.BytesToBitsShift,
&InFlightOrDone[0]
);
}
for (int32 Index = 0; Index < NumQWords; Index++)
{
if (InFlightOrDone[Index] != MAX_uint64)
{
uint64 Mask = InFlightOrDone[Index];
int64 FinalOffset = FirstByte + PAK_CACHE_GRANULARITY * 64 * Index;
while (Mask & 1)
{
FinalOffset += PAK_CACHE_GRANULARITY;
Mask >>= 1;
}
return MakeJoinedRequest(PakIndex, FinalOffset);
}
}
return MAX_uint64;
}
bool AddRequest(TIntervalTreeIndex NewIndex)
{
// CachedFilesScopeLock is locked
FPakInRequest& Request = InRequestAllocator.Get(NewIndex);
uint16 PakIndex = GetRequestPakIndex(Request.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(Request.OffsetAndPakIndex);
FPakData& Pak = CachedPakData[PakIndex];
check(Offset + Request.Size <= Pak.TotalSize && Request.Size > 0 && Request.Priority >= AIOP_MIN && Request.Priority <= AIOP_MAX && Request.Status == EInRequestStatus::Waiting && Request.Owner);
static TArray<uint64> InFlightOrDone;
int64 FirstByte = AlignDown(Offset, PAK_CACHE_GRANULARITY);
int64 LastByte = Align(Offset + Request.Size, PAK_CACHE_GRANULARITY) - 1;
uint32 NumBits = (PAK_CACHE_GRANULARITY + LastByte - FirstByte) / PAK_CACHE_GRANULARITY;
uint32 NumQWords = (NumBits + 63) >> 6;
InFlightOrDone.Reset();
InFlightOrDone.AddZeroed(NumQWords);
if (NumBits != NumQWords * 64)
{
uint32 Extras = NumQWords * 64 - NumBits;
InFlightOrDone[NumQWords - 1] = (MAX_uint64 << (64 - Extras));
}
if (Pak.CacheBlocks[(int32)EBlockStatus::Complete] != IntervalTreeInvalidIndex)
{
Request.Status = EInRequestStatus::Complete;
OverlappingNodesInIntervalTree<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Pak, FirstByte, LastByte](TIntervalTreeIndex Index) -> bool
{
CacheBlockAllocator.Get(Index).InRequestRefCount++;
MaskInterval(Index, CacheBlockAllocator, FirstByte, LastByte, Pak.BytesToBitsShift, &InFlightOrDone[0]);
return true;
}
);
for (int32 Index = 0; Index < NumQWords; Index++)
{
if (InFlightOrDone[Index] != MAX_uint64)
{
Request.Status = EInRequestStatus::Waiting;
break;
}
}
}
if (Request.Status == EInRequestStatus::Waiting)
{
if (Pak.CacheBlocks[(int32)EBlockStatus::InFlight] != IntervalTreeInvalidIndex)
{
Request.Status = EInRequestStatus::InFlight;
OverlappingNodesInIntervalTree<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Pak, FirstByte, LastByte](TIntervalTreeIndex Index) -> bool
{
CacheBlockAllocator.Get(Index).InRequestRefCount++;
MaskInterval(Index, CacheBlockAllocator, FirstByte, LastByte, Pak.BytesToBitsShift, &InFlightOrDone[0]);
return true;
}
);
for (int32 Index = 0; Index < NumQWords; Index++)
{
if (InFlightOrDone[Index] != MAX_uint64)
{
Request.Status = EInRequestStatus::Waiting;
break;
}
}
}
}
else
{
#if DO_CHECK
OverlappingNodesInIntervalTree<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Pak, FirstByte, LastByte](TIntervalTreeIndex Index) -> bool
{
check(0); // if we are complete, then how come there are overlapping in flight blocks?
return true;
}
);
#endif
}
{
AddToIntervalTree<FPakInRequest>(
&Pak.InRequests[Request.Priority][(int32)Request.Status],
InRequestAllocator,
NewIndex,
Pak.StartShift,
Pak.MaxShift
);
}
check(&Request == &InRequestAllocator.Get(NewIndex));
if (Request.Status == EInRequestStatus::Complete)
{
NotifyComplete(NewIndex);
return true;
}
else if (Request.Status == EInRequestStatus::Waiting)
{
StartNextRequest();
}
return false;
}
void ClearBlock(FCacheBlock &Block)
{
#if 0
if (GetRequestPakIndex(Block.OffsetAndPakIndex) == 1 && GetRequestOffset(Block.OffsetAndPakIndex) <= 0x22f573d3ll && GetRequestOffset(Block.OffsetAndPakIndex) + Block.Size >= 0x22f573d3ll)
{
UE_LOG(LogTemp, Error, TEXT("Hi"));
}
#endif
if (Block.Memory)
{
BlockMemory -= Block.Size;
DEC_MEMORY_STAT_BY(STAT_PakCacheMem, Block.Size);
FMemory::Free(Block.Memory);
Block.Memory = nullptr;
}
Block.Next = IntervalTreeInvalidIndex;
CacheBlockAllocator.Free(Block.Index);
}
void ClearRequest(FPakInRequest& DoneRequest)
{
uint64 Id = DoneRequest.UniqueID;
TIntervalTreeIndex Index = DoneRequest.Index;
DoneRequest.OffsetAndPakIndex = 0;
DoneRequest.Size = 0;
DoneRequest.Owner = nullptr;
DoneRequest.UniqueID = 0;
DoneRequest.Index = IntervalTreeInvalidIndex;
DoneRequest.Next = IntervalTreeInvalidIndex;
DoneRequest.Priority = AIOP_MIN;
DoneRequest.Status = EInRequestStatus::Num;
verify(OutstandingRequests.Remove(Id) == 1);
InRequestAllocator.Free(Index);
}
void RemoveRequest(TIntervalTreeIndex Index)
{
// CachedFilesScopeLock is locked
FPakInRequest& Request = InRequestAllocator.Get(Index);
uint16 PakIndex = GetRequestPakIndex(Request.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(Request.OffsetAndPakIndex);
int64 Size = Request.Size;
FPakData& Pak = CachedPakData[PakIndex];
check(Offset + Request.Size <= Pak.TotalSize && Request.Size > 0 && Request.Priority >= AIOP_MIN && Request.Priority <= AIOP_MAX && int32(Request.Status) >= 0 && int32(Request.Status) < int32(EInRequestStatus::Num));
if (RemoveFromIntervalTree<FPakInRequest>(&Pak.InRequests[Request.Priority][(int32)Request.Status], InRequestAllocator, Index, Pak.StartShift, Pak.MaxShift))
{
MaybeRemoveOverlappingNodesInIntervalTree<FCacheBlock>(
&Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
Offset,
Offset + Size - 1,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this](TIntervalTreeIndex BlockIndex) -> bool
{
FCacheBlock &Block = CacheBlockAllocator.Get(BlockIndex);
check(Block.InRequestRefCount);
if (!--Block.InRequestRefCount)
{
ClearBlock(Block);
return true;
}
return false;
}
);
OverlappingNodesInIntervalTree<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
Offset,
Offset + Size - 1,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this](TIntervalTreeIndex BlockIndex) -> bool
{
FCacheBlock &Block = CacheBlockAllocator.Get(BlockIndex);
check(Block.InRequestRefCount);
Block.InRequestRefCount--;
return true;
}
);
}
else
{
check(0); // not found
}
ClearRequest(Request);
}
void NotifyComplete(TIntervalTreeIndex RequestIndex)
{
// CachedFilesScopeLock is locked
FPakInRequest& Request = InRequestAllocator.Get(RequestIndex);
uint16 PakIndex = GetRequestPakIndex(Request.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(Request.OffsetAndPakIndex);
FPakData& Pak = CachedPakData[PakIndex];
check(Offset + Request.Size <= Pak.TotalSize && Request.Size > 0 && Request.Priority >= AIOP_MIN && Request.Priority <= AIOP_MAX && Request.Status == EInRequestStatus::Complete);
check(Request.Owner && Request.UniqueID);
if (Request.Status == EInRequestStatus::Complete && Request.UniqueID == Request.Owner->UniqueID && RequestIndex == Request.Owner->InRequestIndex && Request.OffsetAndPakIndex == Request.Owner->OffsetAndPakIndex)
{
UE_LOG(LogPakFile, Verbose, TEXT("FPakReadRequest[%016llX, %016llX) Notify complete"), Request.OffsetAndPakIndex, Request.OffsetAndPakIndex + Request.Size);
Request.Owner->RequestIsComplete();
return;
}
else
{
check(0); // request should have been found
}
}
FJoinedOffsetAndPakIndex GetNextBlock(EAsyncIOPriority& OutPriority)
{
bool bAcceptingPrecacheRequests = HasEnoughRoomForPrecache();
// CachedFilesScopeLock is locked
uint16 BestPakIndex = 0;
FJoinedOffsetAndPakIndex BestNext = MAX_uint64;
OutPriority = AIOP_MIN;
for (EAsyncIOPriority Priority = AIOP_MAX;; Priority = EAsyncIOPriority(int32(Priority) - 1))
{
if (Priority == AIOP_Precache && !bAcceptingPrecacheRequests)
{
break;
}
for (int32 Pass = 0; ; Pass++)
{
FJoinedOffsetAndPakIndex LocalLastReadRequest = Pass ? 0 : LastReadRequest;
uint16 PakIndex = GetRequestPakIndex(LocalLastReadRequest);
int64 Offset = GetRequestOffset(LocalLastReadRequest);
check(Offset <= CachedPakData[PakIndex].TotalSize);
for (; BestNext == MAX_uint64 && PakIndex < CachedPakData.Num(); PakIndex++)
{
FPakData& Pak = CachedPakData[PakIndex];
if (Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting] != IntervalTreeInvalidIndex)
{
uint64 Limit = uint64(Pak.TotalSize - 1);
if (BestNext != MAX_uint64 && GetRequestPakIndex(BestNext) == PakIndex)
{
Limit = GetRequestOffset(BestNext) - 1;
}
OverlappingNodesInIntervalTreeWithShrinkingInterval<FPakInRequest>(
Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting],
InRequestAllocator,
uint64(Offset),
Limit,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Pak, &BestNext, &BestPakIndex, PakIndex, &Limit, LocalLastReadRequest](TIntervalTreeIndex Index) -> bool
{
FJoinedOffsetAndPakIndex First = FirstUnfilledBlockForRequest(Index, LocalLastReadRequest);
check(LocalLastReadRequest != 0 || First != MAX_uint64); // if there was not trimming, and this thing is in the waiting list, then why was no start block found?
if (First < BestNext)
{
BestNext = First;
BestPakIndex = PakIndex;
Limit = GetRequestOffset(BestNext) - 1;
}
return true; // always have to keep going because we want the smallest one
}
);
}
}
if (!LocalLastReadRequest)
{
break; // this was a full pass
}
}
if (Priority == AIOP_MIN || BestNext != MAX_uint64)
{
OutPriority = Priority;
break;
}
}
return BestNext;
}
bool AddNewBlock()
{
// CachedFilesScopeLock is locked
EAsyncIOPriority RequestPriority;
FJoinedOffsetAndPakIndex BestNext = GetNextBlock(RequestPriority);
if (BestNext == MAX_uint64)
{
return false;
}
uint16 PakIndex = GetRequestPakIndex(BestNext);
int64 Offset = GetRequestOffset(BestNext);
FPakData& Pak = CachedPakData[PakIndex];
check(Offset < Pak.TotalSize);
int64 FirstByte = AlignDown(Offset, PAK_CACHE_GRANULARITY);
int64 LastByte = FMath::Min(Align(FirstByte + PAK_CACHE_MAX_REQUEST, PAK_CACHE_GRANULARITY) - 1, Pak.TotalSize - 1);
check(FirstByte >= 0 && LastByte < Pak.TotalSize && LastByte >= 0 && LastByte >= FirstByte);
uint32 NumBits = (PAK_CACHE_GRANULARITY + LastByte - FirstByte) / PAK_CACHE_GRANULARITY;
uint32 NumQWords = (NumBits + 63) >> 6;
static TArray<uint64> InFlightOrDone;
InFlightOrDone.Reset();
InFlightOrDone.AddZeroed(NumQWords);
if (NumBits != NumQWords * 64)
{
uint32 Extras = NumQWords * 64 - NumBits;
InFlightOrDone[NumQWords - 1] = (MAX_uint64 << (64 - Extras));
}
if (Pak.CacheBlocks[(int32)EBlockStatus::Complete] != IntervalTreeInvalidIndex)
{
OverlappingNodesInIntervalTreeMask<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
Pak.BytesToBitsShift,
&InFlightOrDone[0]
);
}
if (Pak.CacheBlocks[(int32)EBlockStatus::InFlight] != IntervalTreeInvalidIndex)
{
OverlappingNodesInIntervalTreeMask<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
Pak.BytesToBitsShift,
&InFlightOrDone[0]
);
}
static TArray<uint64> Requested;
Requested.Reset();
Requested.AddZeroed(NumQWords);
for (EAsyncIOPriority Priority = AIOP_MAX;; Priority = EAsyncIOPriority(int32(Priority) - 1))
{
if (Priority + PAK_CACHE_MAX_PRIORITY_DIFFERENCE_MERGE < RequestPriority)
{
break;
}
if (Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting] != IntervalTreeInvalidIndex)
{
OverlappingNodesInIntervalTreeMask<FPakInRequest>(
Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting],
InRequestAllocator,
FirstByte,
LastByte,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
Pak.BytesToBitsShift,
&Requested[0]
);
}
if (Priority == AIOP_MIN)
{
break;
}
}
int64 Size = PAK_CACHE_GRANULARITY * 64 * NumQWords;
for (int32 Index = 0; Index < NumQWords; Index++)
{
uint64 NotAlreadyInFlightAndRequested = ((~InFlightOrDone[Index]) & Requested[Index]);
if (NotAlreadyInFlightAndRequested != MAX_uint64)
{
Size = PAK_CACHE_GRANULARITY * 64 * Index;
while (NotAlreadyInFlightAndRequested & 1)
{
Size += PAK_CACHE_GRANULARITY;
NotAlreadyInFlightAndRequested >>= 1;
}
break;
}
}
check(Size > 0 && Size <= PAK_CACHE_MAX_REQUEST);
Size = FMath::Min(FirstByte + Size, LastByte + 1) - FirstByte;
TIntervalTreeIndex NewIndex = CacheBlockAllocator.Alloc();
FCacheBlock& Block = CacheBlockAllocator.Get(NewIndex);
Block.Index = NewIndex;
Block.InRequestRefCount = 0;
Block.Memory = nullptr;
Block.OffsetAndPakIndex = MakeJoinedRequest(PakIndex, FirstByte);
Block.Size = Size;
Block.Status = EBlockStatus::InFlight;
AddToIntervalTree<FCacheBlock>(
&Pak.CacheBlocks[(int32)EBlockStatus::InFlight],
CacheBlockAllocator,
NewIndex,
Pak.StartShift,
Pak.MaxShift
);
TArray<TIntervalTreeIndex> Inflights;
for (EAsyncIOPriority Priority = AIOP_MAX;; Priority = EAsyncIOPriority(int32(Priority) - 1))
{
if (Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting] != IntervalTreeInvalidIndex)
{
MaybeRemoveOverlappingNodesInIntervalTree<FPakInRequest>(
&Pak.InRequests[Priority][(int32)EInRequestStatus::Waiting],
InRequestAllocator,
uint64(FirstByte),
uint64(FirstByte + Size - 1),
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Block, &Inflights](TIntervalTreeIndex RequestIndex) -> bool
{
Block.InRequestRefCount++;
if (FirstUnfilledBlockForRequest(RequestIndex) == MAX_uint64)
{
InRequestAllocator.Get(RequestIndex).Next = IntervalTreeInvalidIndex;
Inflights.Add(RequestIndex);
return true;
}
return false;
}
);
}
#if DO_CHECK
OverlappingNodesInIntervalTree<FPakInRequest>(
Pak.InRequests[Priority][(int32)EInRequestStatus::InFlight],
InRequestAllocator,
uint64(FirstByte),
uint64(FirstByte + Size - 1),
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[](TIntervalTreeIndex) -> bool
{
check(0); // if this is in flight, then why does it overlap my new block
return false;
}
);
OverlappingNodesInIntervalTree<FPakInRequest>(
Pak.InRequests[Priority][(int32)EInRequestStatus::Complete],
InRequestAllocator,
uint64(FirstByte),
uint64(FirstByte + Size - 1),
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[](TIntervalTreeIndex) -> bool
{
check(0); // if this is complete, then why does it overlap my new block
return false;
}
);
#endif
if (Priority == AIOP_MIN)
{
break;
}
}
for (TIntervalTreeIndex Fli : Inflights)
{
FPakInRequest& CompReq = InRequestAllocator.Get(Fli);
CompReq.Status = EInRequestStatus::InFlight;
AddToIntervalTree(&Pak.InRequests[CompReq.Priority][(int32)EInRequestStatus::InFlight], InRequestAllocator, Fli, Pak.StartShift, Pak.MaxShift);
}
StartBlockTask(Block);
return true;
}
int32 OpenTaskSlot()
{
int32 IndexToFill = -1;
for (int32 Index = 0; Index < GPakCache_MaxRequestsToLowerLevel; Index++)
{
if (!RequestsToLower[Index].RequestHandle)
{
IndexToFill = Index;
break;
}
}
return IndexToFill;
}
bool HasRequestsAtStatus(EInRequestStatus Status)
{
for (uint16 PakIndex = 0; PakIndex < CachedPakData.Num(); PakIndex++)
{
FPakData& Pak = CachedPakData[PakIndex];
for (EAsyncIOPriority Priority = AIOP_MAX;; Priority = EAsyncIOPriority(int32(Priority) - 1))
{
if (Pak.InRequests[Priority][(int32)Status] != IntervalTreeInvalidIndex)
{
return true;
}
if (Priority == AIOP_MIN)
{
break;
}
}
}
return false;
}
bool CanStartAnotherTask()
{
if (OpenTaskSlot() < 0)
{
return false;
}
return HasRequestsAtStatus(EInRequestStatus::Waiting);
}
void ClearOldBlockTasks()
{
if (!NotifyRecursion)
{
for (IAsyncReadRequest* Elem : RequestsToDelete)
{
Elem->WaitCompletion();
delete Elem;
}
RequestsToDelete.Empty();
}
}
void StartBlockTask(FCacheBlock& Block)
{
// CachedFilesScopeLock is locked
int32 IndexToFill = OpenTaskSlot();
if (IndexToFill < 0)
{
check(0);
return;
}
EAsyncIOPriority Priority = AIOP_Normal; // the lower level requests are not prioritized at the moment
check(Block.Status == EBlockStatus::InFlight);
UE_LOG(LogPakFile, Verbose, TEXT("FPakReadRequest[%016llX, %016llX) StartBlockTask"), Block.OffsetAndPakIndex, Block.OffsetAndPakIndex + Block.Size);
uint16 PakIndex = GetRequestPakIndex(Block.OffsetAndPakIndex);
FPakData& Pak = CachedPakData[PakIndex];
RequestsToLower[IndexToFill].BlockIndex = Block.Index;
check(&CacheBlockAllocator.Get(RequestsToLower[IndexToFill].BlockIndex) == &Block);
RequestsToLower[IndexToFill].RequestHandle = Pak.Handle->ReadRequest(GetRequestOffset(Block.OffsetAndPakIndex), Block.Size, Priority, &CallbackFromLower);
LastReadRequest = Block.OffsetAndPakIndex + Block.Size;
Loads++;
LoadSize += Block.Size;
}
void CompleteRequest(bool bWasCanceled, IAsyncReadRequest* Request, TIntervalTreeIndex BlockIndex)
{
FCacheBlock& Block = CacheBlockAllocator.Get(BlockIndex);
uint16 PakIndex = GetRequestPakIndex(Block.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(Block.OffsetAndPakIndex);
FPakData& Pak = CachedPakData[PakIndex];
check(!Block.Memory && Block.Size);
check(!bWasCanceled); // this is doable, but we need to transition requests back to waiting, inflight etc.
if (!RemoveFromIntervalTree<FCacheBlock>(&Pak.CacheBlocks[(int32)EBlockStatus::InFlight], CacheBlockAllocator, Block.Index, Pak.StartShift, Pak.MaxShift))
{
check(0);
}
if (Block.InRequestRefCount == 0 || bWasCanceled)
{
ClearBlock(Block);
}
else
{
Block.Memory = Request->GetReadResults();
BlockMemory += Block.Size;
INC_MEMORY_STAT_BY(STAT_PakCacheMem, Block.Size);
if (BlockMemory > BlockMemoryHighWater)
{
BlockMemoryHighWater = BlockMemory;
static int64 LastPrint = 0;
if (BlockMemoryHighWater / 1024 / 1024 != LastPrint)
{
LastPrint = BlockMemoryHighWater / 1024 / 1024;
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Precache HighWater %dMB\r\n"), int32(LastPrint));
}
}
Block.Status = EBlockStatus::Complete;
AddToIntervalTree<FCacheBlock>(
&Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
Block.Index,
Pak.StartShift,
Pak.MaxShift
);
TArray<TIntervalTreeIndex> Completeds;
for (EAsyncIOPriority Priority = AIOP_MAX;; Priority = EAsyncIOPriority(int32(Priority) - 1))
{
if (Pak.InRequests[Priority][(int32)EInRequestStatus::InFlight] != IntervalTreeInvalidIndex)
{
MaybeRemoveOverlappingNodesInIntervalTree<FPakInRequest>(
&Pak.InRequests[Priority][(int32)EInRequestStatus::InFlight],
InRequestAllocator,
uint64(Offset),
uint64(Offset + Block.Size - 1),
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, &Completeds](TIntervalTreeIndex RequestIndex) -> bool
{
if (FirstUnfilledBlockForRequest(RequestIndex) == MAX_uint64)
{
InRequestAllocator.Get(RequestIndex).Next = IntervalTreeInvalidIndex;
Completeds.Add(RequestIndex);
return true;
}
return false;
}
);
}
if (Priority == AIOP_MIN)
{
break;
}
}
for (TIntervalTreeIndex Comp : Completeds)
{
FPakInRequest& CompReq = InRequestAllocator.Get(Comp);
CompReq.Status = EInRequestStatus::Complete;
AddToIntervalTree(&Pak.InRequests[CompReq.Priority][(int32)EInRequestStatus::Complete], InRequestAllocator, Comp, Pak.StartShift, Pak.MaxShift);
NotifyComplete(Comp); // potentially scary recursion here
}
}
}
bool StartNextRequest()
{
if (CanStartAnotherTask())
{
return AddNewBlock();
}
return false;
}
uint8* GetCompletedRequestData(FPakInRequest& DoneRequest)
{
// CachedFilesScopeLock is locked
check(DoneRequest.Status == EInRequestStatus::Complete);
uint16 PakIndex = GetRequestPakIndex(DoneRequest.OffsetAndPakIndex);
int64 Offset = GetRequestOffset(DoneRequest.OffsetAndPakIndex);
int64 Size = DoneRequest.Size;
FPakData& Pak = CachedPakData[PakIndex];
check(Offset + DoneRequest.Size <= Pak.TotalSize && DoneRequest.Size > 0 && DoneRequest.Priority >= AIOP_MIN && DoneRequest.Priority <= AIOP_MAX && DoneRequest.Status == EInRequestStatus::Complete);
uint8* Result = (uint8*)FMemory::Malloc(Size);
int64 BytesCopied = 0;
#if 0 // this path removes the block in one pass, however, this is not what we want because it wrecks precaching, if we change back GetCompletedRequest needs to maybe start a new request and the logic of the IAsyncFile read needs to change
MaybeRemoveOverlappingNodesInIntervalTree<FCacheBlock>(
&Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
Offset,
Offset + Size - 1,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, Offset, Size, &BytesCopied, Result, &Pak](TIntervalTreeIndex BlockIndex) -> bool
{
FCacheBlock &Block = CacheBlockAllocator.Get(BlockIndex);
int64 BlockOffset = GetRequestOffset(Block.OffsetAndPakIndex);
check(Block.Memory && Block.Size && BlockOffset >= 0 && BlockOffset + Block.Size <= Pak.TotalSize);
int64 OverlapStart = FMath::Max(Offset, BlockOffset);
int64 OverlapEnd = FMath::Min(Offset + Size, BlockOffset + Block.Size);
check(OverlapEnd > OverlapStart);
BytesCopied += OverlapEnd - OverlapStart;
FMemory::Memcpy(Result + OverlapStart - Offset, Block.Memory + OverlapStart - BlockOffset, OverlapEnd - OverlapStart);
check(Block.InRequestRefCount);
if (!--Block.InRequestRefCount)
{
ClearBlock(Block);
return true;
}
return false;
}
);
if (!RemoveFromIntervalTree<FPakInRequest>(&Pak.InRequests[DoneRequest.Priority][(int32)EInRequestStatus::Complete], InRequestAllocator, DoneRequest.Index, Pak.StartShift, Pak.MaxShift))
{
check(0); // not found
}
ClearRequest(DoneRequest);
#else
OverlappingNodesInIntervalTree<FCacheBlock>(
Pak.CacheBlocks[(int32)EBlockStatus::Complete],
CacheBlockAllocator,
Offset,
Offset + Size - 1,
0,
Pak.MaxNode,
Pak.StartShift,
Pak.MaxShift,
[this, Offset, Size, &BytesCopied, Result, &Pak](TIntervalTreeIndex BlockIndex) -> bool
{
FCacheBlock &Block = CacheBlockAllocator.Get(BlockIndex);
int64 BlockOffset = GetRequestOffset(Block.OffsetAndPakIndex);
check(Block.Memory && Block.Size && BlockOffset >= 0 && BlockOffset + Block.Size <= Pak.TotalSize);
int64 OverlapStart = FMath::Max(Offset, BlockOffset);
int64 OverlapEnd = FMath::Min(Offset + Size, BlockOffset + Block.Size);
check(OverlapEnd > OverlapStart);
BytesCopied += OverlapEnd - OverlapStart;
FMemory::Memcpy(Result + OverlapStart - Offset, Block.Memory + OverlapStart - BlockOffset, OverlapEnd - OverlapStart);
return true;
}
);
#endif
check(BytesCopied == Size);
return Result;
}
///// Below here are the thread entrypoints
void NewRequestsToLowerComplete(bool bWasCanceled, IAsyncReadRequest* Request)
{
FScopeLock Lock(&CachedFilesScopeLock);
ClearOldBlockTasks();
NotifyRecursion++;
for (int32 Index = 0; Index < PAK_CACHE_MAX_REQUESTS; Index++) // we loop over them all in case GPakCache_MaxRequestsToLowerLevel is changing
{
if (RequestsToLower[Index].RequestHandle == Request)
{
CompleteRequest(bWasCanceled, Request, RequestsToLower[Index].BlockIndex);
RequestsToLower[Index].RequestHandle = nullptr;
RequestsToDelete.Add(Request);
RequestsToLower[Index].BlockIndex = IntervalTreeInvalidIndex;
StartNextRequest();
NotifyRecursion--;
return;
}
}
StartNextRequest();
NotifyRecursion--;
check(0); // not found?
}
public:
bool QueueRequest(IPakRequestor* Owner, FName File, int64 PakFileSize, int64 Offset, int64 Size, EAsyncIOPriority Priority)
{
check(Owner && File != NAME_None && Size > 0 && Offset >= 0 && Offset < PakFileSize && Priority >= AIOP_MIN && Priority <= AIOP_MAX);
FScopeLock Lock(&CachedFilesScopeLock);
uint16* PakIndexPtr = CachedPaks.Find(File);
if (!PakIndexPtr)
{
check(CachedPakData.Num() < MAX_uint16);
IAsyncReadFileHandle* Handle = LowerLevel->OpenAsyncRead(*File.ToString());
if (!Handle)
{
return false;
}
CachedPakData.Add(FPakData(Handle, File, PakFileSize));
PakIndexPtr = &CachedPaks.Add(File, CachedPakData.Num() - 1);
}
uint16 PakIndex = *PakIndexPtr;
FPakData& Pak = CachedPakData[PakIndex];
check(Pak.Name == File && Pak.TotalSize == PakFileSize);
TIntervalTreeIndex RequestIndex = InRequestAllocator.Alloc();
FPakInRequest& Request = InRequestAllocator.Get(RequestIndex);
FJoinedOffsetAndPakIndex RequestOffsetAndPakIndex = MakeJoinedRequest(PakIndex, Offset);
Request.OffsetAndPakIndex = RequestOffsetAndPakIndex;
Request.Size = Size;
Request.Priority = Priority;
Request.Status = EInRequestStatus::Waiting;
Request.Owner = Owner;
Request.UniqueID = NextUniqueID++;
Request.Index = RequestIndex;
check(Request.Next == IntervalTreeInvalidIndex);
Owner->OffsetAndPakIndex = Request.OffsetAndPakIndex;
Owner->UniqueID = Request.UniqueID;
Owner->InRequestIndex = RequestIndex;
check(!OutstandingRequests.Contains(Request.UniqueID));
OutstandingRequests.Add(Request.UniqueID, RequestIndex);
if (AddRequest(RequestIndex))
{
UE_LOG(LogPakFile, Verbose, TEXT("FPakReadRequest[%016llX, %016llX) QueueRequest HOT"), RequestOffsetAndPakIndex, RequestOffsetAndPakIndex + Request.Size);
}
else
{
UE_LOG(LogPakFile, Verbose, TEXT("FPakReadRequest[%016llX, %016llX) QueueRequest COLD"), RequestOffsetAndPakIndex, RequestOffsetAndPakIndex + Request.Size);
}
return true;
}
uint8* GetCompletedRequest(IPakRequestor* Owner)
{
check(Owner);
FScopeLock Lock(&CachedFilesScopeLock);
ClearOldBlockTasks();
TIntervalTreeIndex RequestIndex = OutstandingRequests.FindRef(Owner->UniqueID);
static_assert(IntervalTreeInvalidIndex == 0, "FindRef will return 0 for something not found");
if (RequestIndex)
{
FPakInRequest& Request = InRequestAllocator.Get(RequestIndex);
check(Owner == Request.Owner && Request.Status == EInRequestStatus::Complete && Request.UniqueID == Request.Owner->UniqueID && RequestIndex == Request.Owner->InRequestIndex && Request.OffsetAndPakIndex == Request.Owner->OffsetAndPakIndex);
uint8* Result = GetCompletedRequestData(Request);
return Result;
}
return nullptr; // canceled
}
void CancelRequest(IPakRequestor* Owner)
{
check(Owner);
FScopeLock Lock(&CachedFilesScopeLock);
ClearOldBlockTasks();
TIntervalTreeIndex RequestIndex = OutstandingRequests.FindRef(Owner->UniqueID);
static_assert(IntervalTreeInvalidIndex == 0, "FindRef will return 0 for something not found");
if (RequestIndex)
{
bool bWasOver = !HasEnoughRoomForPrecache();
FPakInRequest& Request = InRequestAllocator.Get(RequestIndex);
check(Owner == Request.Owner && Request.UniqueID == Request.Owner->UniqueID && RequestIndex == Request.Owner->InRequestIndex && Request.OffsetAndPakIndex == Request.Owner->OffsetAndPakIndex);
RemoveRequest(RequestIndex);
bool bNowOver = !HasEnoughRoomForPrecache();
if (bWasOver && !bNowOver)
{
StartNextRequest();
}
}
}
bool IsProbablyIdle() // nothing to prevent new requests from being made before I return
{
FScopeLock Lock(&CachedFilesScopeLock);
return !HasRequestsAtStatus(EInRequestStatus::Waiting) && !HasRequestsAtStatus(EInRequestStatus::InFlight);
}
// these are not threadsafe and should only be used for synthetic testing
uint64 GetLoadSize()
{
return LoadSize;
}
uint32 GetLoads()
{
return Loads;
}
uint32 GetFrees()
{
return Frees;
}
void DumpBlocks()
{
while (!FPakPrecacher::Get().IsProbablyIdle())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_WaitDumpBlocks);
FPlatformProcess::SleepNoStats(0.001f);
}
FScopeLock Lock(&CachedFilesScopeLock);
bool bDone = !HasRequestsAtStatus(EInRequestStatus::Waiting) && !HasRequestsAtStatus(EInRequestStatus::InFlight) && !HasRequestsAtStatus(EInRequestStatus::Complete);
if (!bDone)
{
UE_LOG(LogPakFile, Log, TEXT("PakCache has outstanding requests with %llu total memory."), BlockMemory);
}
else
{
UE_LOG(LogPakFile, Log, TEXT("PakCache has no outstanding requests with %llu total memory."), BlockMemory);
}
}
};
static void WaitPrecache(const TArray<FString>& Args)
{
uint32 Frees = FPakPrecacher::Get().GetFrees();
uint32 Loads = FPakPrecacher::Get().GetLoads();
uint64 LoadSize = FPakPrecacher::Get().GetLoadSize();
double StartTime = FPlatformTime::Seconds();
while (!FPakPrecacher::Get().IsProbablyIdle())
{
check(Frees == FPakPrecacher::Get().GetFrees()); // otherwise we are discarding things, which is not what we want for this synthetic test
QUICK_SCOPE_CYCLE_COUNTER(STAT_WaitPrecache);
FPlatformProcess::SleepNoStats(0.001f);
}
Loads = FPakPrecacher::Get().GetLoads() - Loads;
LoadSize = FPakPrecacher::Get().GetLoadSize() - LoadSize;
float TimeSpent = FPlatformTime::Seconds() - StartTime;
float LoadSizeMB = float(LoadSize) / (1024.0f * 1024.0f);
float MBs = LoadSizeMB / TimeSpent;
UE_LOG(LogPakFile, Log, TEXT("Loaded %4d blocks (align %4dKB) totalling %7.2fMB in %4.2fs = %6.2fMB/s"), Loads, PAK_CACHE_GRANULARITY / 1024, LoadSizeMB, TimeSpent, MBs);
}
static FAutoConsoleCommand WaitPrecacheCmd(
TEXT("pak.WaitPrecache"),
TEXT("Debug command to wait on the pak precache."),
FConsoleCommandWithArgsDelegate::CreateStatic(&WaitPrecache)
);
static void FlushPrecache(const TArray<FString>& Args)
{
}
static FAutoConsoleCommand FlushPrecacheCmd(
TEXT("pak.FlushPrecache"),
TEXT("Does nothing (yet)."),
FConsoleCommandWithArgsDelegate::CreateStatic(&FlushPrecache)
);
static void DumpBlocks(const TArray<FString>& Args)
{
FPakPrecacher::Get().DumpBlocks();
}
static FAutoConsoleCommand DumpBlocksCmd(
TEXT("pak.DumpBlocks"),
TEXT("Debug command to spew the outstanding blocks."),
FConsoleCommandWithArgsDelegate::CreateStatic(&DumpBlocks)
);
static FCriticalSection FPakReadRequestEvent;
class FPakReadRequest : public IAsyncReadRequest, public IPakRequestor
{
int64 Offset;
int64 BytesToRead;
FEvent* WaitEvent;
EAsyncIOPriority Priority;
bool bRequestOutstanding;
bool bNeedsRemoval;
public:
FPakReadRequest(FName InPakFile, int64 PakFileSize, FAsyncFileCallBack* CompleteCallback, int64 InOffset, int64 InBytesToRead, EAsyncIOPriority InPriority)
: IAsyncReadRequest(CompleteCallback, false)
, Offset(InOffset)
, BytesToRead(InBytesToRead)
, WaitEvent(nullptr)
, Priority(InPriority)
, bRequestOutstanding(true)
, bNeedsRemoval(true)
{
check(Offset >= 0 && BytesToRead > 0);
if (!FPakPrecacher::Get().QueueRequest(this, InPakFile, PakFileSize, Offset, BytesToRead, Priority))
{
bRequestOutstanding = false;
SetComplete();
}
}
virtual ~FPakReadRequest()
{
if (bNeedsRemoval)
{
FPakPrecacher::Get().CancelRequest(this);
}
}
// IAsyncReadRequest Interface
virtual void WaitCompletionImpl(float TimeLimitSeconds) override
{
if (bRequestOutstanding)
{
{
FScopeLock Lock(&FPakReadRequestEvent);
if (bRequestOutstanding)
{
check(!WaitEvent);
WaitEvent = FPlatformProcess::GetSynchEventFromPool(true);
}
}
if (WaitEvent)
{
if (TimeLimitSeconds == 0.0f)
{
WaitEvent->Wait();
check(!bRequestOutstanding);
}
else
{
WaitEvent->Wait(TimeLimitSeconds * 1000.0f);
}
FPlatformProcess::ReturnSynchEventToPool(WaitEvent);
WaitEvent = nullptr;
}
}
}
virtual void CancelImpl() override
{
check(!WaitEvent); // you canceled from a different thread that you waited from
FPakPrecacher::Get().CancelRequest(this);
bNeedsRemoval = false;
if (bRequestOutstanding)
{
bRequestOutstanding = false;
SetComplete();
}
check(!WaitEvent); // you canceled from a different thread that you waited from
}
// IPakRequestor Interface
virtual void RequestIsComplete() override
{
check(bRequestOutstanding && !Memory);
if (bCanceled || Priority <= AIOP_Precache)
{
}
else
{
Memory = FPakPrecacher::Get().GetCompletedRequest(this);
// Memory could be null here if the request was canceled
}
SetComplete();
FScopeLock Lock(&FPakReadRequestEvent);
bRequestOutstanding = false;
if (WaitEvent)
{
WaitEvent->Trigger();
}
}
};
class FPakSizeRequest : public IAsyncReadRequest
{
public:
FPakSizeRequest(FAsyncFileCallBack* CompleteCallback, int64 InFileSize)
: IAsyncReadRequest(CompleteCallback, true)
{
Size = InFileSize;
SetComplete();
}
virtual void WaitCompletionImpl(float TimeLimitSeconds) override
{
}
virtual void CancelImpl()
{
}
};
class FPakAsyncReadFileHandle final : public IAsyncReadFileHandle
{
FName PakFile;
int64 PakFileSize;
int64 OffsetInPak;
int64 FileSize;
public:
FPakAsyncReadFileHandle(FName InPakFile, int64 InPakFileSize, int64 InOffsetInPak, int64 InFileSize)
: PakFile(InPakFile)
, PakFileSize(InPakFileSize)
, OffsetInPak(InOffsetInPak)
, FileSize(InFileSize)
{
check(PakFileSize > 0 && OffsetInPak + FileSize <= PakFileSize && OffsetInPak >= 0);
}
virtual IAsyncReadRequest* SizeRequest(FAsyncFileCallBack* CompleteCallback = nullptr) override
{
return new FPakSizeRequest(CompleteCallback, FileSize);
}
virtual IAsyncReadRequest* ReadRequest(int64 Offset, int64 BytesToRead, EAsyncIOPriority Priority = AIOP_Normal, FAsyncFileCallBack* CompleteCallback = nullptr) override
{
if (BytesToRead == MAX_int64)
{
BytesToRead = FileSize - Offset;
}
check(Offset + BytesToRead <= FileSize && Offset >= 0 && Offset + BytesToRead + OffsetInPak <= PakFileSize);
return new FPakReadRequest(PakFile, PakFileSize, CompleteCallback, OffsetInPak + Offset, BytesToRead, Priority);
}
};
#endif
#if USE_NEW_ASYNC_IO
IAsyncReadFileHandle* FPakPlatformFile::OpenAsyncRead(const TCHAR* Filename)
{
#if USE_PAK_PRECACHE
if (GPakCache_Enable > 0)
{
FPakFile* PakFile = NULL;
const FPakEntry* FileEntry = FindFileInPakFiles(Filename, &PakFile);
if (FileEntry && PakFile && PakFile->GetFilenameName() != NAME_None)
{
int64 OffsetToFile = FileEntry->Offset + FileEntry->GetSerializedSize(PakFile->GetInfo().Version);
UE_LOG(LogPakFile, Verbose, TEXT("FPakPlatformFile::OpenAsyncRead[%016llX, %016llX) %s"), OffsetToFile, OffsetToFile + FileEntry->UncompressedSize, Filename);
return new FPakAsyncReadFileHandle(PakFile->GetFilenameName(), PakFile->TotalSize(), OffsetToFile, FileEntry->UncompressedSize);
}
}
#endif
return IPlatformFile::OpenAsyncRead(Filename);
}
#endif // USE_NEW_ASYNC_IO
/**
* Class to handle correctly reading from a compressed file within a compressed package
*/
class FPakSimpleEncryption
{
public:
enum
{
Alignment = FAES::AESBlockSize,
};
static FORCEINLINE int64 AlignReadRequest(int64 Size)
{
return Align(Size, Alignment);
}
static FORCEINLINE void DecryptBlock(void* Data, int64 Size)
{
#ifdef AES_KEY
FAES::DecryptData((uint8*)Data, Size);
#endif
}
};
/**
* Thread local class to manage working buffers for file compression
*/
class FCompressionScratchBuffers : public TThreadSingleton<FCompressionScratchBuffers>
{
public:
FCompressionScratchBuffers()
: TempBufferSize(0)
, ScratchBufferSize(0)
{}
int64 TempBufferSize;
TAutoPtr<uint8> TempBuffer;
int64 ScratchBufferSize;
TAutoPtr<uint8> ScratchBuffer;
void EnsureBufferSpace(int64 CompressionBlockSize, int64 ScrachSize)
{
if(TempBufferSize < CompressionBlockSize)
{
TempBufferSize = CompressionBlockSize;
TempBuffer.Reset((uint8*)FMemory::Malloc(TempBufferSize));
}
if(ScratchBufferSize < ScrachSize)
{
ScratchBufferSize = ScrachSize;
ScratchBuffer.Reset((uint8*)FMemory::Malloc(ScratchBufferSize));
}
}
};
/**
* Class to handle correctly reading from a compressed file within a pak
*/
template< typename EncryptionPolicy = FPakNoEncryption >
class FPakCompressedReaderPolicy
{
public:
class FPakUncompressTask : public FNonAbandonableTask
{
public:
uint8* UncompressedBuffer;
int32 UncompressedSize;
uint8* CompressedBuffer;
int32 CompressedSize;
ECompressionFlags Flags;
void* CopyOut;
int64 CopyOffset;
int64 CopyLength;
void DoWork()
{
// Decrypt and Uncompress from memory to memory.
int64 EncryptionSize = EncryptionPolicy::AlignReadRequest(CompressedSize);
EncryptionPolicy::DecryptBlock(CompressedBuffer, EncryptionSize);
FCompression::UncompressMemory(Flags, UncompressedBuffer, UncompressedSize, CompressedBuffer, CompressedSize, false, FPlatformMisc::GetPlatformCompression()->GetCompressionBitWindow());
if (CopyOut)
{
FMemory::Memcpy(CopyOut, UncompressedBuffer+CopyOffset, CopyLength);
}
}
FORCEINLINE TStatId GetStatId() const
{
// TODO: This is called too early in engine startup.
return TStatId();
//RETURN_QUICK_DECLARE_CYCLE_STAT(FPakUncompressTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
FPakCompressedReaderPolicy(const FPakFile& InPakFile, const FPakEntry& InPakEntry, FArchive* InPakReader)
: PakFile(InPakFile)
, PakEntry(InPakEntry)
, PakReader(InPakReader)
{
}
/** Pak file that own this file data */
const FPakFile& PakFile;
/** Pak file entry for this file. */
const FPakEntry& PakEntry;
/** Pak file archive to read the data from. */
FArchive* PakReader;
FORCEINLINE int64 FileSize() const
{
return PakEntry.UncompressedSize;
}
void Serialize(int64 DesiredPosition, void* V, int64 Length)
{
const int32 CompressionBlockSize = PakEntry.CompressionBlockSize;
uint32 CompressionBlockIndex = DesiredPosition / CompressionBlockSize;
uint8* WorkingBuffers[2];
int64 DirectCopyStart = DesiredPosition % PakEntry.CompressionBlockSize;
FAsyncTask<FPakUncompressTask> UncompressTask;
FCompressionScratchBuffers& ScratchSpace = FCompressionScratchBuffers::Get();
bool bStartedUncompress = false;
int64 WorkingBufferRequiredSize = FCompression::CompressMemoryBound((ECompressionFlags)PakEntry.CompressionMethod,CompressionBlockSize, FPlatformMisc::GetPlatformCompression()->GetCompressionBitWindow());
WorkingBufferRequiredSize = EncryptionPolicy::AlignReadRequest(WorkingBufferRequiredSize);
ScratchSpace.EnsureBufferSpace(CompressionBlockSize, WorkingBufferRequiredSize*2);
WorkingBuffers[0] = ScratchSpace.ScratchBuffer;
WorkingBuffers[1] = ScratchSpace.ScratchBuffer + WorkingBufferRequiredSize;
while (Length > 0)
{
const FPakCompressedBlock& Block = PakEntry.CompressionBlocks[CompressionBlockIndex];
int64 Pos = CompressionBlockIndex * CompressionBlockSize;
int64 CompressedBlockSize = Block.CompressedEnd-Block.CompressedStart;
int64 UncompressedBlockSize = FMath::Min<int64>(PakEntry.UncompressedSize-Pos, PakEntry.CompressionBlockSize);
int64 ReadSize = EncryptionPolicy::AlignReadRequest(CompressedBlockSize);
int64 WriteSize = FMath::Min<int64>(UncompressedBlockSize - DirectCopyStart, Length);
PakReader->Seek(Block.CompressedStart);
PakReader->Serialize(WorkingBuffers[CompressionBlockIndex & 1],ReadSize);
if (bStartedUncompress)
{
UncompressTask.EnsureCompletion();
bStartedUncompress = false;
}
FPakUncompressTask& TaskDetails = UncompressTask.GetTask();
if (DirectCopyStart == 0 && Length >= CompressionBlockSize)
{
// Block can be decompressed directly into output buffer
TaskDetails.Flags = (ECompressionFlags)PakEntry.CompressionMethod;
TaskDetails.UncompressedBuffer = (uint8*)V;
TaskDetails.UncompressedSize = UncompressedBlockSize;
TaskDetails.CompressedBuffer = WorkingBuffers[CompressionBlockIndex & 1];
TaskDetails.CompressedSize = CompressedBlockSize;
TaskDetails.CopyOut = nullptr;
}
else
{
// Block needs to be copied from a working buffer
TaskDetails.Flags = (ECompressionFlags)PakEntry.CompressionMethod;
TaskDetails.UncompressedBuffer = (uint8*)ScratchSpace.TempBuffer;
TaskDetails.UncompressedSize = UncompressedBlockSize;
TaskDetails.CompressedBuffer = WorkingBuffers[CompressionBlockIndex & 1];
TaskDetails.CompressedSize = CompressedBlockSize;
TaskDetails.CopyOut = V;
TaskDetails.CopyOffset = DirectCopyStart;
TaskDetails.CopyLength = WriteSize;
}
if (Length == WriteSize)
{
UncompressTask.StartSynchronousTask();
}
else
{
UncompressTask.StartBackgroundTask();
}
bStartedUncompress = true;
V = (void*)((uint8*)V + WriteSize);
Length -= WriteSize;
DirectCopyStart = 0;
++CompressionBlockIndex;
}
if(bStartedUncompress)
{
UncompressTask.EnsureCompletion();
}
}
};
bool FPakEntry::VerifyPakEntriesMatch(const FPakEntry& FileEntryA, const FPakEntry& FileEntryB)
{
bool bResult = true;
if (FileEntryA.Size != FileEntryB.Size)
{
UE_LOG(LogPakFile, Error, TEXT("Pak header file size mismatch, got: %lld, expected: %lld"), FileEntryB.Size, FileEntryA.Size);
bResult = false;
}
if (FileEntryA.UncompressedSize != FileEntryB.UncompressedSize)
{
UE_LOG(LogPakFile, Error, TEXT("Pak header uncompressed file size mismatch, got: %lld, expected: %lld"), FileEntryB.UncompressedSize, FileEntryA.UncompressedSize);
bResult = false;
}
if (FileEntryA.CompressionMethod != FileEntryB.CompressionMethod)
{
UE_LOG(LogPakFile, Error, TEXT("Pak header file compression method mismatch, got: %d, expected: %d"), FileEntryB.CompressionMethod, FileEntryA.CompressionMethod);
bResult = false;
}
if (FMemory::Memcmp(FileEntryA.Hash, FileEntryB.Hash, sizeof(FileEntryA.Hash)) != 0)
{
UE_LOG(LogPakFile, Error, TEXT("Pak file hash does not match its index entry"));
bResult = false;
}
return bResult;
}
bool FPakPlatformFile::IsNonPakFilenameAllowed(const FString& InFilename)
{
FFilenameSecurityDelegate& FilenameSecurityDelegate = GetFilenameSecurityDelegate();
if (!FilenameSecurityDelegate.IsBound())
{
return true;
}
if (GetLowerLevel()->FileExists(*InFilename))
{
return FilenameSecurityDelegate.Execute(*InFilename);
}
else
{
return false;
}
}
FPakFile::FPakFile(const TCHAR* Filename, bool bIsSigned)
: PakFilename(Filename)
, PakFilenameName(Filename)
, CachedTotalSize(0)
, bSigned(bIsSigned)
, bIsValid(false)
{
FArchive* Reader = GetSharedReader(NULL);
if (Reader)
{
Timestamp = IFileManager::Get().GetTimeStamp(Filename);
Initialize(Reader);
}
}
FPakFile::FPakFile(IPlatformFile* LowerLevel, const TCHAR* Filename, bool bIsSigned)
: PakFilename(Filename)
, PakFilenameName(Filename)
, CachedTotalSize(0)
, bSigned(bIsSigned)
, bIsValid(false)
{
FArchive* Reader = GetSharedReader(LowerLevel);
if (Reader)
{
Timestamp = LowerLevel->GetTimeStamp(Filename);
Initialize(Reader);
}
}
FPakFile::FPakFile(FArchive* Archive)
: bSigned(false)
, bIsValid(false)
{
Initialize(Archive);
}
FPakFile::~FPakFile()
{
}
FArchive* FPakFile::CreatePakReader(const TCHAR* Filename)
{
FArchive* ReaderArchive = IFileManager::Get().CreateFileReader(Filename);
return SetupSignedPakReader(ReaderArchive, Filename);
}
FArchive* FPakFile::CreatePakReader(IFileHandle& InHandle, const TCHAR* Filename)
{
FArchive* ReaderArchive = new FArchiveFileReaderGeneric(&InHandle, Filename, InHandle.Size());
return SetupSignedPakReader(ReaderArchive, Filename);
}
FArchive* FPakFile::SetupSignedPakReader(FArchive* ReaderArchive, const TCHAR* Filename)
{
if (FPlatformProperties::RequiresCookedData())
{
#if !USING_SIGNED_CONTENT
if (bSigned || FParse::Param(FCommandLine::Get(), TEXT("signedpak")) || FParse::Param(FCommandLine::Get(), TEXT("signed")))
#endif
{
if (!Decryptor.IsValid())
{
Decryptor = new FChunkCacheWorker(ReaderArchive, Filename);
}
ReaderArchive = new FSignedArchiveReader(ReaderArchive, Decryptor);
}
}
return ReaderArchive;
}
void FPakFile::Initialize(FArchive* Reader)
{
CachedTotalSize = Reader->TotalSize();
if (CachedTotalSize < Info.GetSerializedSize())
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted pak file (too short). Verify your installation."));
}
else
{
// Serialize trailer and check if everything is as expected.
Reader->Seek(CachedTotalSize - Info.GetSerializedSize());
Info.Serialize(*Reader);
UE_CLOG(Info.Magic != FPakInfo::PakFile_Magic, LogPakFile, Fatal, TEXT("Trailing magic number (%ud) is different than the expected one. Verify your installation."), Info.Magic);
UE_CLOG(!(Info.Version >= FPakInfo::PakFile_Version_Initial && Info.Version <= FPakInfo::PakFile_Version_Latest), LogPakFile, Fatal, TEXT("Invalid pak file version (%d). Verify your installation."), Info.Version);
LoadIndex(Reader);
// LoadIndex should crash in case of an error, so just assume everything is ok if we got here.
bIsValid = true;
if (FParse::Param(FCommandLine::Get(), TEXT("checkpak")))
{
ensure(Check());
}
}
}
void FPakFile::LoadIndex(FArchive* Reader)
{
if (CachedTotalSize < (Info.IndexOffset + Info.IndexSize))
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index offset in pak file."));
}
else
{
// Load index into memory first.
Reader->Seek(Info.IndexOffset);
TArray<uint8> IndexData;
IndexData.AddUninitialized(Info.IndexSize);
Reader->Serialize(IndexData.GetData(), Info.IndexSize);
FMemoryReader IndexReader(IndexData);
// Check SHA1 value.
uint8 IndexHash[20];
FSHA1::HashBuffer(IndexData.GetData(), IndexData.Num(), IndexHash);
if (FMemory::Memcmp(IndexHash, Info.IndexHash, sizeof(IndexHash)) != 0)
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index in pak file (CRC mismatch)."));
}
// Read the default mount point and all entries.
int32 NumEntries = 0;
IndexReader << MountPoint;
IndexReader << NumEntries;
MakeDirectoryFromPath(MountPoint);
// Allocate enough memory to hold all entries (and not reallocate while they're being added to it).
Files.Empty(NumEntries);
for (int32 EntryIndex = 0; EntryIndex < NumEntries; EntryIndex++)
{
// Serialize from memory.
FPakEntry Entry;
FString Filename;
IndexReader << Filename;
Entry.Serialize(IndexReader, Info.Version);
// Add new file info.
Files.Add(Entry);
// Construct Index of all directories in pak file.
FString Path = FPaths::GetPath(Filename);
MakeDirectoryFromPath(Path);
FPakDirectory* Directory = Index.Find(Path);
if (Directory != NULL)
{
Directory->Add(Filename, &Files.Last());
}
else
{
FPakDirectory NewDirectory;
NewDirectory.Add(Filename, &Files.Last());
Index.Add(Path, NewDirectory);
// add the parent directories up to the mount point
while (MountPoint != Path)
{
Path = Path.Left(Path.Len()-1);
int32 Offset = 0;
if (Path.FindLastChar('/', Offset))
{
Path = Path.Left(Offset);
MakeDirectoryFromPath(Path);
if (Index.Find(Path) == NULL)
{
FPakDirectory ParentDirectory;
Index.Add(Path, ParentDirectory);
}
}
else
{
Path = MountPoint;
}
}
}
}
}
}
bool FPakFile::Check()
{
UE_LOG(LogPakFile, Display, TEXT("Checking pak file \"%s\". This may take a while..."), *PakFilename);
FArchive& PakReader = *GetSharedReader(NULL);
int32 ErrorCount = 0;
int32 FileCount = 0;
for (FPakFile::FFileIterator It(*this); It; ++It, ++FileCount)
{
const FPakEntry& Entry = It.Info();
void* FileContents = FMemory::Malloc(Entry.Size);
PakReader.Seek(Entry.Offset);
uint32 SerializedCrcTest = 0;
FPakEntry EntryInfo;
EntryInfo.Serialize(PakReader, GetInfo().Version);
if (EntryInfo != Entry)
{
UE_LOG(LogPakFile, Error, TEXT("Serialized hash mismatch for \"%s\"."), *It.Filename());
ErrorCount++;
}
PakReader.Serialize(FileContents, Entry.Size);
uint8 TestHash[20];
FSHA1::HashBuffer(FileContents, Entry.Size, TestHash);
if (FMemory::Memcmp(TestHash, Entry.Hash, sizeof(TestHash)) != 0)
{
UE_LOG(LogPakFile, Error, TEXT("Hash mismatch for \"%s\"."), *It.Filename());
ErrorCount++;
}
else
{
UE_LOG(LogPakFile, Display, TEXT("\"%s\" OK."), *It.Filename());
}
FMemory::Free(FileContents);
}
if (ErrorCount == 0)
{
UE_LOG(LogPakFile, Display, TEXT("Pak file \"%s\" healthy, %d files checked."), *PakFilename, FileCount);
}
else
{
UE_LOG(LogPakFile, Display, TEXT("Pak file \"%s\" corrupted (%d errors ouf of %d files checked.)."), *PakFilename, ErrorCount, FileCount);
}
return ErrorCount == 0;
}
FArchive* FPakFile::GetSharedReader(IPlatformFile* LowerLevel)
{
uint32 Thread = FPlatformTLS::GetCurrentThreadId();
FArchive* PakReader = NULL;
{
FScopeLock ScopedLock(&CriticalSection);
TAutoPtr<FArchive>* ExistingReader = ReaderMap.Find(Thread);
if (ExistingReader)
{
PakReader = *ExistingReader;
}
}
if (!PakReader)
{
// Create a new FArchive reader and pass it to the new handle.
if (LowerLevel != NULL)
{
IFileHandle* PakHandle = LowerLevel->OpenRead(*GetFilename());
if (PakHandle)
{
PakReader = CreatePakReader(*PakHandle, *GetFilename());
}
}
else
{
PakReader = CreatePakReader(*GetFilename());
}
if (!PakReader)
{
UE_LOG(LogPakFile, Fatal, TEXT("Unable to create pak \"%s\" handle"), *GetFilename());
}
{
FScopeLock ScopedLock(&CriticalSection);
ReaderMap.Emplace(Thread, PakReader);
}
}
return PakReader;
}
#if !UE_BUILD_SHIPPING
class FPakExec : private FSelfRegisteringExec
{
FPakPlatformFile& PlatformFile;
public:
FPakExec(FPakPlatformFile& InPlatformFile)
: PlatformFile(InPlatformFile)
{}
/** Console commands **/
virtual bool Exec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) override
{
if (FParse::Command(&Cmd, TEXT("Mount")))
{
PlatformFile.HandleMountCommand(Cmd, Ar);
return true;
}
if (FParse::Command(&Cmd, TEXT("Unmount")))
{
PlatformFile.HandleUnmountCommand(Cmd, Ar);
return true;
}
else if (FParse::Command(&Cmd, TEXT("PakList")))
{
PlatformFile.HandlePakListCommand(Cmd, Ar);
return true;
}
return false;
}
};
static TAutoPtr<FPakExec> GPakExec;
void FPakPlatformFile::HandleMountCommand(const TCHAR* Cmd, FOutputDevice& Ar)
{
const FString PakFilename = FParse::Token(Cmd, false);
if (!PakFilename.IsEmpty())
{
const FString MountPoint = FParse::Token(Cmd, false);
Mount(*PakFilename, 0, MountPoint.IsEmpty() ? NULL : *MountPoint);
}
}
void FPakPlatformFile::HandleUnmountCommand(const TCHAR* Cmd, FOutputDevice& Ar)
{
const FString PakFilename = FParse::Token(Cmd, false);
if (!PakFilename.IsEmpty())
{
Unmount(*PakFilename);
}
}
void FPakPlatformFile::HandlePakListCommand(const TCHAR* Cmd, FOutputDevice& Ar)
{
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
for (auto Pak : Paks)
{
Ar.Logf(TEXT("%s"), *Pak.PakFile->GetFilename());
}
}
#endif // !UE_BUILD_SHIPPING
FPakPlatformFile::FPakPlatformFile()
: LowerLevel(NULL)
, bSigned(false)
{
}
FPakPlatformFile::~FPakPlatformFile()
{
FCoreDelegates::OnMountPak.Unbind();
FCoreDelegates::OnUnmountPak.Unbind();
// We need to flush async IO... if it hasn't been shut down already.
if (FIOSystem::HasShutdown() == false)
{
FIOSystem& IOSystem = FIOSystem::Get();
IOSystem.BlockTillAllRequestsFinishedAndFlushHandles();
}
{
FScopeLock ScopedLock(&PakListCritical);
for (int32 PakFileIndex = 0; PakFileIndex < PakFiles.Num(); PakFileIndex++)
{
delete PakFiles[PakFileIndex].PakFile;
PakFiles[PakFileIndex].PakFile = nullptr;
}
}
}
void FPakPlatformFile::FindPakFilesInDirectory(IPlatformFile* LowLevelFile, const TCHAR* Directory, TArray<FString>& OutPakFiles)
{
// Helper class to find all pak files.
class FPakSearchVisitor : public IPlatformFile::FDirectoryVisitor
{
TArray<FString>& FoundPakFiles;
IPlatformChunkInstall* ChunkInstall;
public:
FPakSearchVisitor(TArray<FString>& InFoundPakFiles, IPlatformChunkInstall* InChunkInstall)
: FoundPakFiles(InFoundPakFiles)
, ChunkInstall(InChunkInstall)
{}
virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory)
{
if (bIsDirectory == false)
{
FString Filename(FilenameOrDirectory);
if (FPaths::GetExtension(Filename) == TEXT("pak"))
{
// if a platform supports chunk style installs, make sure that the chunk a pak file resides in is actually fully installed before accepting pak files from it
if (ChunkInstall)
{
FString ChunkIdentifier(TEXT("pakchunk"));
FString BaseFilename = FPaths::GetBaseFilename(Filename);
if (BaseFilename.StartsWith(ChunkIdentifier))
{
int32 DelimiterIndex = 0;
int32 StartOfChunkIndex = ChunkIdentifier.Len();
BaseFilename.FindChar(TEXT('-'), DelimiterIndex);
FString ChunkNumberString = BaseFilename.Mid(StartOfChunkIndex, DelimiterIndex-StartOfChunkIndex);
int32 ChunkNumber = 0;
TTypeFromString<int32>::FromString(ChunkNumber, *ChunkNumberString);
if (ChunkInstall->GetChunkLocation(ChunkNumber) == EChunkLocation::NotAvailable)
{
return true;
}
}
}
FoundPakFiles.Add(Filename);
}
}
return true;
}
};
// Find all pak files.
FPakSearchVisitor Visitor(OutPakFiles, FPlatformMisc::GetPlatformChunkInstall());
LowLevelFile->IterateDirectoryRecursively(Directory, Visitor);
}
void FPakPlatformFile::FindAllPakFiles(IPlatformFile* LowLevelFile, const TArray<FString>& PakFolders, TArray<FString>& OutPakFiles)
{
// Find pak files from the specified directories.
for (int32 FolderIndex = 0; FolderIndex < PakFolders.Num(); ++FolderIndex)
{
FindPakFilesInDirectory(LowLevelFile, *PakFolders[FolderIndex], OutPakFiles);
}
}
void FPakPlatformFile::GetPakFolders(const TCHAR* CmdLine, TArray<FString>& OutPakFolders)
{
#if !UE_BUILD_SHIPPING
// Command line folders
FString PakDirs;
if (FParse::Value(CmdLine, TEXT("-pakdir="), PakDirs))
{
TArray<FString> CmdLineFolders;
PakDirs.ParseIntoArray(CmdLineFolders, TEXT("*"), true);
OutPakFolders.Append(CmdLineFolders);
}
#endif
// @todo plugin urgent: Needs to handle plugin Pak directories, too
// Hardcoded locations
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::GameContentDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::GameSavedDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::EngineContentDir()));
}
bool FPakPlatformFile::CheckIfPakFilesExist(IPlatformFile* LowLevelFile, const TArray<FString>& PakFolders)
{
TArray<FString> FoundPakFiles;
FindAllPakFiles(LowLevelFile, PakFolders, FoundPakFiles);
return FoundPakFiles.Num() > 0;
}
bool FPakPlatformFile::ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const
{
#if !USING_SIGNED_CONTENT
bool Result = FParse::Param(CmdLine, TEXT("Pak")) || FParse::Param(CmdLine, TEXT("Signedpak")) || FParse::Param(CmdLine, TEXT("Signed"));
if (FPlatformProperties::RequiresCookedData() && !Result && !FParse::Param(CmdLine, TEXT("NoPak")))
{
TArray<FString> PakFolders;
GetPakFolders(CmdLine, PakFolders);
Result = CheckIfPakFilesExist(Inner, PakFolders);
}
return Result;
#else
return true;
#endif
}
bool FPakPlatformFile::Initialize(IPlatformFile* Inner, const TCHAR* CmdLine)
{
// Inner is required.
check(Inner != NULL);
LowerLevel = Inner;
#if !USING_SIGNED_CONTENT
bSigned = FParse::Param(CmdLine, TEXT("Signedpak")) || FParse::Param(CmdLine, TEXT("Signed"));
if (!bSigned)
{
// Even if -signed is not provided in the command line, use signed reader if the hardcoded key is non-zero.
FEncryptionKey DecryptionKey;
DecryptionKey.Exponent.Parse(DECRYPTION_KEY_EXPONENT);
DecryptionKey.Modulus.Parse(DECRYPTION_KEY_MODULUS);
bSigned = !DecryptionKey.Exponent.IsZero() && !DecryptionKey.Modulus.IsZero();
}
#else
bSigned = true;
#endif
bool bMountPaks = true;
TArray<FString> PaksToLoad;
#if !UE_BUILD_SHIPPING
// Optionally get a list of pak filenames to load, only these paks will be mounted
FString CmdLinePaksToLoad;
if (FParse::Value(CmdLine, TEXT("-paklist="), CmdLinePaksToLoad))
{
CmdLinePaksToLoad.ParseIntoArray(PaksToLoad, TEXT("+"), true);
}
//if we are using a fileserver, then dont' mount paks automatically. We only want to read files from the server.
FString FileHostIP;
const bool bCookOnTheFly = FParse::Value(FCommandLine::Get(), TEXT("filehostip"), FileHostIP);
bMountPaks = !bCookOnTheFly;
#endif
if (bMountPaks)
{
// Find and mount pak files from the specified directories.
TArray<FString> PakFolders;
GetPakFolders(CmdLine, PakFolders);
TArray<FString> FoundPakFiles;
FindAllPakFiles(LowerLevel, PakFolders, FoundPakFiles);
// Sort in descending order.
FoundPakFiles.Sort(TGreater<FString>());
// Mount all found pak files
for (int32 PakFileIndex = 0; PakFileIndex < FoundPakFiles.Num(); PakFileIndex++)
{
const FString& PakFilename = FoundPakFiles[PakFileIndex];
bool bLoadPak = true;
if (PaksToLoad.Num() && !PaksToLoad.Contains(FPaths::GetBaseFilename(PakFilename)))
{
bLoadPak = false;
}
if (bLoadPak)
{
// hardcode default load ordering of game main pak -> game content -> engine content -> saved dir
// would be better to make this config but not even the config system is initialized here so we can't do that
uint32 PakOrder = 0;
if (PakFilename.StartsWith(FString::Printf(TEXT("%sPaks/%s-"), *FPaths::GameContentDir(), FApp::GetGameName())))
{
PakOrder = 4;
}
else if (PakFilename.StartsWith(FPaths::GameContentDir()))
{
PakOrder = 3;
}
else if (PakFilename.StartsWith(FPaths::EngineContentDir()))
{
PakOrder = 2;
}
else if (PakFilename.StartsWith(FPaths::GameSavedDir()))
{
PakOrder = 1;
}
Mount(*PakFilename, PakOrder);
}
}
}
#if !UE_BUILD_SHIPPING
GPakExec = new FPakExec(*this);
#endif // !UE_BUILD_SHIPPING
FCoreDelegates::OnMountPak.BindRaw(this, &FPakPlatformFile::HandleMountPakDelegate);
FCoreDelegates::OnUnmountPak.BindRaw(this, &FPakPlatformFile::HandleUnmountPakDelegate);
#if USE_PAK_PRECACHE
FPakPrecacher::Init(LowerLevel);
#endif
return !!LowerLevel;
}
bool FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath /*= NULL*/)
{
bool bSuccess = false;
TSharedPtr<IFileHandle> PakHandle = MakeShareable(LowerLevel->OpenRead(InPakFilename));
if (PakHandle.IsValid())
{
FPakFile* Pak = new FPakFile(LowerLevel, InPakFilename, bSigned);
if (Pak->IsValid())
{
if (InPath != NULL)
{
Pak->SetMountPoint(InPath);
}
FString PakFilename = InPakFilename;
if ( PakFilename.EndsWith(TEXT("_P.pak")) )
{
PakOrder += 100;
}
{
// Add new pak file
FScopeLock ScopedLock(&PakListCritical);
FPakListEntry Entry;
Entry.ReadOrder = PakOrder;
Entry.PakFile = Pak;
PakFiles.Add(Entry);
PakFiles.StableSort();
}
bSuccess = true;
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Failed to mount pak \"%s\", pak is invalid."), InPakFilename);
}
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Pak \"%s\" does not exist!"), InPakFilename);
}
return bSuccess;
}
bool FPakPlatformFile::Unmount(const TCHAR* InPakFilename)
{
{
FScopeLock ScopedLock(&PakListCritical);
for (int32 PakIndex = 0; PakIndex < PakFiles.Num(); PakIndex++)
{
if (PakFiles[PakIndex].PakFile->GetFilename() == InPakFilename)
{
delete PakFiles[PakIndex].PakFile;
PakFiles.RemoveAt(PakIndex);
return true;
}
}
}
return false;
}
IFileHandle* FPakPlatformFile::CreatePakFileHandle(const TCHAR* Filename, FPakFile* PakFile, const FPakEntry* FileEntry)
{
IFileHandle* Result = NULL;
bool bNeedsDelete = true;
FArchive* PakReader = PakFile->GetSharedReader(LowerLevel);
// Create the handle.
if (FileEntry->CompressionMethod != COMPRESS_None && PakFile->GetInfo().Version >= FPakInfo::PakFile_Version_CompressionEncryption)
{
if (FileEntry->bEncrypted)
{
Result = new FPakFileHandle< FPakCompressedReaderPolicy<FPakSimpleEncryption> >(*PakFile, *FileEntry, PakReader, bNeedsDelete);
}
else
{
Result = new FPakFileHandle< FPakCompressedReaderPolicy<> >(*PakFile, *FileEntry, PakReader, bNeedsDelete);
}
}
else if (FileEntry->bEncrypted)
{
Result = new FPakFileHandle< FPakReaderPolicy<FPakSimpleEncryption> >(*PakFile, *FileEntry, PakReader, bNeedsDelete);
}
else
{
Result = new FPakFileHandle<>(*PakFile, *FileEntry, PakReader, bNeedsDelete);
}
return Result;
}
bool FPakPlatformFile::HandleMountPakDelegate(const FString& PakFilePath, uint32 PakOrder, IPlatformFile::FDirectoryVisitor* Visitor)
{
bool bReturn = Mount(*PakFilePath, PakOrder);
if (bReturn && Visitor != nullptr)
{
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
// Find the single pak we just mounted
for (auto Pak : Paks)
{
if (PakFilePath == Pak.PakFile->GetFilename())
{
// Get a list of all of the files in the pak
for (FPakFile::FFileIterator It(*Pak.PakFile); It; ++It)
{
Visitor->Visit(*It.Filename(), false);
}
return true;
}
}
}
return bReturn;
}
bool FPakPlatformFile::HandleUnmountPakDelegate(const FString& PakFilePath)
{
return Unmount(*PakFilePath);
}
IFileHandle* FPakPlatformFile::OpenRead(const TCHAR* Filename, bool bAllowWrite)
{
IFileHandle* Result = NULL;
FPakFile* PakFile = NULL;
const FPakEntry* FileEntry = FindFileInPakFiles(Filename, &PakFile);
if (FileEntry != NULL)
{
Result = CreatePakFileHandle(Filename, PakFile, FileEntry);
}
#if !USING_SIGNED_CONTENT
else
{
if (IsNonPakFilenameAllowed(Filename))
{
// Default to wrapped file
Result = LowerLevel->OpenRead(Filename, bAllowWrite);
}
}
#endif
return Result;
}
bool FPakPlatformFile::BufferedCopyFile(IFileHandle& Dest, IFileHandle& Source, const int64 FileSize, uint8* Buffer, const int64 BufferSize) const
{
int64 RemainingSizeToCopy = FileSize;
// Continue copying chunks using the buffer
while (RemainingSizeToCopy > 0)
{
const int64 SizeToCopy = FMath::Min(BufferSize, RemainingSizeToCopy);
if (Source.Read(Buffer, SizeToCopy) == false)
{
return false;
}
if (Dest.Write(Buffer, SizeToCopy) == false)
{
return false;
}
RemainingSizeToCopy -= SizeToCopy;
}
return true;
}
bool FPakPlatformFile::CopyFile(const TCHAR* To, const TCHAR* From)
{
bool Result = false;
FPakFile* PakFile = NULL;
const FPakEntry* FileEntry = FindFileInPakFiles(From, &PakFile);
if (FileEntry != NULL)
{
// Copy from pak to LowerLevel->
// Create handles both files.
TAutoPtr<IFileHandle> DestHandle(LowerLevel->OpenWrite(To));
TAutoPtr<IFileHandle> SourceHandle(CreatePakFileHandle(From, PakFile, FileEntry));
if (DestHandle.IsValid() && SourceHandle.IsValid())
{
const int64 BufferSize = 64 * 1024; // Copy in 64K chunks.
uint8* Buffer = (uint8*)FMemory::Malloc(BufferSize);
Result = BufferedCopyFile(*DestHandle, *SourceHandle, SourceHandle->Size(), Buffer, BufferSize);
FMemory::Free(Buffer);
}
}
else
{
Result = LowerLevel->CopyFile(To, From);
}
return Result;
}
uint32 ComputePakChunkHash(const uint8* InData, const int64 InDataSize)
{
return FCrc::MemCrc32(InData, InDataSize);
}
/**
* Module for the pak file
*/
class FPakFileModule : public IPlatformFileModule
{
public:
virtual IPlatformFile* GetPlatformFile() override
{
static TScopedPointer<IPlatformFile> AutoDestroySingleton(new FPakPlatformFile());
return AutoDestroySingleton.GetOwnedPointer();
}
};
IMPLEMENT_MODULE(FPakFileModule, PakFile);