UefiPayloadPkg/FmpDeviceSmmLib: improve capsule update progress bar

Update the progress bar upon each block being read, not only after
reading the whole flash - and likewise for writing.

This improves the UX in the sense that the progress bar starts moving
at a steady pace right away, so that the update doesn't seem stuck.

Scale per-block write weight by (BlockCount / DifferingBlocks) instead
of shrinking TotalUnits; this keeps the denominator stable so progress
advances smoothly rather than jumping when only a fraction of blocks
differ.

The smooth progress bar feature is gated by a PCD that can
be passed by coreboot. It's autoenabled if CAPSULES_V2 is set.

Signed-off-by: Filip Lewinski <filip.lewinski@3mdeb.com>
This commit is contained in:
Filip Lewiński
2026-01-28 20:19:05 +01:00
committed by Sergii Dmytruk
parent 82b0e4d4d4
commit a72c7105e5
6 changed files with 259 additions and 59 deletions
+4
View File
@@ -114,3 +114,7 @@ gDasharoPayloadPkgTokenSpaceGuid.PcdShowCapsuleLogo|FALSE|BOOLEAN|0x0000000D
## Enables best-effort firmware recovery after hitting an I/O error during
## firmware update. When FALSE, no recovery is attempted.
gDasharoPayloadPkgTokenSpaceGuid.PcdCapsuleRecovery|FALSE|BOOLEAN|0x0000000E
## Enables smooth per-block progress reporting during capsule updates.
## When FALSE, progress assumes all blocks differ (coarser reporting).
gDasharoPayloadPkgTokenSpaceGuid.PcdSmoothCapsuleProgress|FALSE|BOOLEAN|0x0000000F
+1
View File
@@ -560,6 +560,7 @@ OrderedCollectionLib|MdePkg/Library/BaseOrderedCollectionRedBlackTreeLib/BaseOrd
gDasharoPayloadPkgTokenSpaceGuid.PcdShowCapsuleReport|TRUE
gDasharoPayloadPkgTokenSpaceGuid.PcdShowCapsuleLogo|TRUE
gDasharoPayloadPkgTokenSpaceGuid.PcdCapsuleRecovery|TRUE
gDasharoPayloadPkgTokenSpaceGuid.PcdSmoothCapsuleProgress|TRUE
!endif
[PcdsPatchableInModule.common]
@@ -200,14 +200,21 @@ GetFmap (
}
/**
Read current firmware in full and return as newly allocated pool memory.
Read current firmware in full and return as newly allocated pool memory,
with optional progress reporting via callback.
@param[in] IsDescriptorLocked Whether the flash descriptor is locked.
@param[in] ProgressCallback Optional callback invoked after each block read.
@param[in] ProgressContext Caller-provided context passed to callback.
@return NULL On error.
**/
VOID *
EFIAPI
ReadCurrentFirmware (
BOOLEAN IsDescriptorLocked
IN BOOLEAN IsDescriptorLocked,
IN FW_READ_PROGRESS_CALLBACK ProgressCallback OPTIONAL,
IN VOID *ProgressContext OPTIONAL
)
{
EFI_STATUS Status;
@@ -216,6 +223,7 @@ ReadCurrentFirmware (
UINTN Block;
UINTN BlockSize;
UINTN NumBytes;
UINTN BlockCount;
Status = SmmStoreLibGetBlockSize (&BlockSize);
if (EFI_ERROR (Status)) {
@@ -251,7 +259,9 @@ ReadCurrentFirmware (
return NULL;
}
for (Block = 0; Block < FwSize / BlockSize; Block++) {
BlockCount = FwSize / BlockSize;
for (Block = 0; Block < BlockCount; Block++) {
NumBytes = BlockSize;
Status = SmmStoreLibReadAnyBlock (
Block,
@@ -279,6 +289,10 @@ ReadCurrentFirmware (
return NULL;
}
}
if (ProgressCallback != NULL) {
ProgressCallback (Block, BlockCount, ProgressContext);
}
}
return Image;
@@ -1081,7 +1095,7 @@ MergeFirmwareImages (
}
if (!MigrateRecoveryMrcCache (&Data)) {
DEBUG ((DEBUG_ERROR, "%a(): MigrateMrcCache () failed\n", __FUNCTION__));
DEBUG ((DEBUG_ERROR, "%a(): MigrateRecoveryMrcCache () failed\n", __FUNCTION__));
goto Fail;
}
@@ -2,14 +2,34 @@
#define FLASHING_H__
/**
Read current firmware in full and return as newly allocated pool memory.
Callback function type for reporting read progress during firmware read.
@param[in] CurrentBlock The block that was just read (0-indexed).
@param[in] TotalBlocks Total number of blocks to read.
@param[in] Context Caller-provided context pointer.
**/
typedef VOID (*FW_READ_PROGRESS_CALLBACK)(
UINTN CurrentBlock,
UINTN TotalBlocks,
VOID *Context
);
/**
Read current firmware in full and return as newly allocated pool memory,
with optional progress reporting via callback.
@param[in] IsDescriptorLocked Whether the flash descriptor is locked.
@param[in] ProgressCallback Optional callback invoked after each block read.
@param[in] ProgressContext Caller-provided context passed to callback.
@return NULL On error.
**/
VOID *
EFIAPI
ReadCurrentFirmware (
BOOLEAN IsDescriptorLocked
IN BOOLEAN IsDescriptorLocked,
IN FW_READ_PROGRESS_CALLBACK ProgressCallback OPTIONAL,
IN VOID *ProgressContext OPTIONAL
);
/**
@@ -729,42 +729,47 @@ FmpDeviceSetImage (
);
}
/**
Advances progress of the operation by a single step, converts the steps to
percents and invokes a callback if there is one.
//
// Weight constants for progress calculation.
// Reading 8 blocks takes approximately the same time as 1 erase+write operation.
//
#define READ_BLOCK_WEIGHT 1
#define UPDATE_BLOCK_WEIGHT 8
@param[in] Callback External callback for progress reporting
or NULL if there is none.
@param[in] TotalSteps Total number of flashing steps.
@param[in] By How many steps to advance.
@param[in, out] Step Current step number.
@param[in, out] ShouldReportProgress A flag indicating whether progress needs
to be reported.
/**
Context structure for tracking progress during firmware update.
**/
typedef struct {
EFI_FIRMWARE_MANAGEMENT_UPDATE_IMAGE_PROGRESS Callback;
UINTN TotalUnits;
UINTN CurrentUnits;
BOOLEAN ShouldReport;
} ProgressContext;
/**
Reports current progress as a percentage.
@param[in, out] Ctx Progress context to update and report from.
**/
STATIC
VOID
IncrementProgress (
IN EFI_FIRMWARE_MANAGEMENT_UPDATE_IMAGE_PROGRESS Callback OPTIONAL,
IN UINTN TotalSteps,
IN UINTN By,
IN OUT UINTN *Step,
IN OUT BOOLEAN *ShouldReportProgress
ReportProgress (
IN OUT ProgressContext *Ctx
)
{
EFI_STATUS Status;
UINTN Progress;
if (!*ShouldReportProgress) {
if (!Ctx->ShouldReport || Ctx->Callback == NULL) {
Ctx->ShouldReport = FALSE;
return;
}
if (Callback == NULL) {
*ShouldReportProgress = FALSE;
if (Ctx->TotalUnits == 0) {
return;
}
*Step += By;
Progress = (*Step * 100) / TotalSteps;
Progress = (Ctx->CurrentUnits * 100) / Ctx->TotalUnits;
//
// Value of 0 means "progress reporting is not supported", so avoid using it.
@@ -773,13 +778,94 @@ IncrementProgress (
Progress = 1;
}
Status = Callback (Progress);
//
// Cap at 100%
//
if (Progress > 100) {
Progress = 100;
}
Status = Ctx->Callback (Progress);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_WARN, "%a(): progress callback failed with: %r\n", __func__, Status));
*ShouldReportProgress = FALSE;
Ctx->ShouldReport = FALSE;
}
}
/**
Callback invoked after each block is read during firmware reading.
@param[in] CurrentBlock The block that was just read (0-indexed).
@param[in] TotalBlocks Total number of blocks to read.
@param[in] Context Pointer to ProgressContext.
**/
STATIC
VOID
ReadProgressCallback (
UINTN CurrentBlock,
UINTN TotalBlocks,
VOID *Context
)
{
ProgressContext *Ctx;
if (Context == NULL) {
return;
}
Ctx = (ProgressContext *)Context;
Ctx->CurrentUnits += READ_BLOCK_WEIGHT;
ReportProgress (Ctx);
}
/**
Counts the number of blocks that differ between two firmware images.
@param[in] Image1 First image to compare.
@param[in] Image2 Second image to compare.
@param[in] Size Total size of images in bytes.
@param[in] BlockSize Size of each block in bytes.
@return Number of blocks that differ between the two images.
**/
STATIC
UINTN
CountDifferingBlocks (
IN CONST UINT8 *Image1,
IN CONST UINT8 *Image2,
IN UINTN Size,
IN UINTN BlockSize,
IN BOOLEAN DescriptorLocked
)
{
UINTN BlockCount;
UINTN Block;
UINTN DifferingCount;
UINTN Offset;
if (BlockSize == 0) {
return 0;
}
BlockCount = Size / BlockSize;
DifferingCount = 0;
Offset = 0;
for (Block = 0; Block < BlockCount; Block++, Offset += BlockSize) {
if (CompareMem (Image1 + Offset, Image2 + Offset, BlockSize) == 0) {
continue;
}
if (!IsRangeWriteable (Image1, Size, Offset, BlockSize)) {
continue;
}
DifferingCount++;
}
return DifferingCount;
}
/**
This code finds variable in storage blocks (Volatile or Non-Volatile).
@@ -1020,7 +1106,7 @@ AttemptRecovery (
return FALSE;
}
CurrentImage = ReadCurrentFirmware (DescriptorLocked);
CurrentImage = ReadCurrentFirmware (DescriptorLocked, NULL, NULL);
if (CurrentImage == NULL) {
DEBUG ((
DEBUG_ERROR,
@@ -1074,7 +1160,7 @@ AttemptRecovery (
// Judge outcome of the recovery by re-reading the firmware and comparing it
// against the original.
//
CurrentImage = ReadCurrentFirmware (DescriptorLocked);
CurrentImage = ReadCurrentFirmware (DescriptorLocked, NULL, NULL);
if (CurrentImage == NULL) {
DEBUG ((
DEBUG_ERROR,
@@ -1184,21 +1270,22 @@ FmpDeviceSetImageWithStatus (
OUT UINT32 *LastAttemptStatus
)
{
EFI_STATUS Status;
UINTN BlockSize;
UINTN BlockCount;
UINTN Block;
UINTN NumBytes;
UINTN TotalSteps;
UINTN ReadSteps;
UINTN Step;
BOOLEAN ShouldReportProgress;
UINT8 *CurrentImage;
UINT8 *UpdatedImage;
UINTN Offset;
BOOLEAN DescriptorLocked;
CHAR16 *ErrorString = NULL;
UINT32 AttemptSlotB;
EFI_STATUS Status;
UINTN BlockSize;
UINTN BlockCount;
UINTN Block;
UINTN NumBytes;
UINT8 *CurrentImage;
UINT8 *UpdatedImage;
UINTN Offset;
BOOLEAN DescriptorLocked;
CHAR16 *ErrorString = NULL;
UINT32 AttemptSlotB;
ProgressContext ProgressCtx;
UINTN DifferingBlocks;
UINTN WrittenBlocks;
UINTN ScaledWriteWeight;
BOOLEAN SmoothProgress;
*LastAttemptStatus = LAST_ATTEMPT_STATUS_ERROR_UNSUCCESSFUL;
@@ -1228,17 +1315,32 @@ FmpDeviceSetImageWithStatus (
BlockSize
));
ShouldReportProgress = TRUE;
TotalSteps = BlockCount * 2; // Erase and write of each block.
Step = 0;
SmoothProgress = FixedPcdGetBool (PcdSmoothCapsuleProgress);
// Allocate 5% of progress for reading the flash.
ReadSteps = (TotalSteps * 5) / 100;
TotalSteps += ReadSteps;
//
// Initialize progress context.
// Phase 1 (reading): BlockCount * READ_BLOCK_WEIGHT units
// Phase 2 (writing): BlockCount * UPDATE_BLOCK_WEIGHT units (worst case)
//
// When SmoothProgress is enabled, TotalUnits is recalculated after merge
// based on actual differing blocks for accurate progress reporting.
//
ProgressCtx.Callback = Progress;
ProgressCtx.CurrentUnits = 0;
ProgressCtx.TotalUnits = BlockCount * READ_BLOCK_WEIGHT + BlockCount * UPDATE_BLOCK_WEIGHT;
ProgressCtx.ShouldReport = TRUE;
DescriptorLocked = IsDescriptorLocked();
CurrentImage = ReadCurrentFirmware (DescriptorLocked);
//
// Phase 1: Read current firmware.
// When SmoothProgress is enabled, report progress after each block read.
//
CurrentImage = ReadCurrentFirmware (
DescriptorLocked,
SmoothProgress ? ReadProgressCallback : NULL,
SmoothProgress ? &ProgressCtx : NULL
);
if (CurrentImage == NULL) {
DEBUG ((
DEBUG_ERROR,
@@ -1248,7 +1350,10 @@ FmpDeviceSetImageWithStatus (
return EFI_END_OF_MEDIA;
}
IncrementProgress (Progress, TotalSteps, ReadSteps, &Step, &ShouldReportProgress);
if (!SmoothProgress) {
ProgressCtx.CurrentUnits = BlockCount * READ_BLOCK_WEIGHT;
ReportProgress (&ProgressCtx);
}
if (!AreImageBtgKeysCompatible(CurrentImage, Image, ImageSize)) {
FreePool (CurrentImage);
@@ -1278,15 +1383,49 @@ FmpDeviceSetImageWithStatus (
return EFI_ABORTED;
}
//
// Phase 2: Write differing blocks.
//
ScaledWriteWeight = UPDATE_BLOCK_WEIGHT;
if (SmoothProgress) {
//
// Count how many blocks actually differ to get accurate progress.
//
DifferingBlocks = CountDifferingBlocks (
CurrentImage,
UpdatedImage,
ImageSize,
BlockSize,
DescriptorLocked
);
DEBUG ((DEBUG_INFO, "%a(): %d blocks differ out of %d total\n",
__FUNCTION__, DifferingBlocks, BlockCount));
//
// Scale the per-block write weight so that writing all differing blocks
// still consumes exactly BlockCount * UPDATE_BLOCK_WEIGHT units, keeping
// TotalUnits unchanged and avoiding a sudden progress jump.
//
if (DifferingBlocks > 0) {
ScaledWriteWeight = UPDATE_BLOCK_WEIGHT * BlockCount / DifferingBlocks;
}
}
Offset = 0;
WrittenBlocks = 0;
for (Block = 0; Block < BlockCount; Block++, Offset += BlockSize) {
//
// Save the flash and time by only writing a block if new contents differs
// from the old one.
//
if (CompareMem (CurrentImage + Offset, UpdatedImage + Offset, BlockSize) == 0) {
// Erase and write steps.
IncrementProgress (Progress, TotalSteps, 2, &Step, &ShouldReportProgress);
if (!SmoothProgress) {
//
// All blocks counted in TotalUnits, so advance progress for identical ones.
//
ProgressCtx.CurrentUnits += UPDATE_BLOCK_WEIGHT;
ReportProgress (&ProgressCtx);
}
continue;
}
@@ -1300,7 +1439,14 @@ FmpDeviceSetImageWithStatus (
"%a(): range %d - %d is not writeable\n",
__FUNCTION__, Offset, BlockSize + Offset
));
IncrementProgress (Progress, TotalSteps, 2, &Step, &ShouldReportProgress);
if (!SmoothProgress) {
//
// In non-smooth mode all blocks are counted in TotalUnits, so advance
// progress for non-writeable blocks too.
//
ProgressCtx.CurrentUnits += UPDATE_BLOCK_WEIGHT;
ReportProgress (&ProgressCtx);
}
continue;
}
@@ -1309,7 +1455,11 @@ FmpDeviceSetImageWithStatus (
goto IoError;
}
IncrementProgress (Progress, TotalSteps, 1, &Step, &ShouldReportProgress);
//
// Report progress after erase (half of write weight).
//
ProgressCtx.CurrentUnits += ScaledWriteWeight / 2;
ReportProgress (&ProgressCtx);
NumBytes = BlockSize;
Status = SmmStoreLibWriteAnyBlock (Block, 0, &NumBytes, UpdatedImage + Offset);
@@ -1317,9 +1467,17 @@ FmpDeviceSetImageWithStatus (
goto IoError;
}
IncrementProgress (Progress, TotalSteps, 1, &Step, &ShouldReportProgress);
//
// Report progress after write (remaining half of write weight).
//
ProgressCtx.CurrentUnits += ScaledWriteWeight - (ScaledWriteWeight / 2);
ReportProgress (&ProgressCtx);
WrittenBlocks++;
}
DEBUG ((DEBUG_INFO, "%a(): wrote %d blocks\n", __FUNCTION__, WrittenBlocks));
FreePool (CurrentImage);
FreePool (UpdatedImage);
@@ -56,6 +56,9 @@
[Protocols]
gEfiSmbiosProtocolGuid ## CONSUMES
[Pcd]
gDasharoPayloadPkgTokenSpaceGuid.PcdSmoothCapsuleProgress ## CONSUMES
[Guids]
gEfiFirmwareInfoHobGuid
gDasharoSystemFeaturesGuid