/** @file

  A DXE_RUNTIME_DRIVER providing synchronous SMI activations via the
  EFI_SMM_CONTROL2_PROTOCOL.

  We expect the PEI phase to have covered the following:
  - ensure that the underlying QEMU machine type be Q35
    (responsible: OvmfPkg/SmmAccess/SmmAccessPei.inf)
  - ensure that the ACPI PM IO space be configured
    (responsible: OvmfPkg/PlatformPei/PlatformPei.inf)

  Our own entry point is responsible for confirming the SMI feature and for
  configuring it.

  Copyright (C) 2013, 2015, Red Hat, Inc.<BR>
  Copyright (c) 2009 - 2010, Intel Corporation. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <IndustryStandard/Q35MchIch9.h>
#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/PcdLib.h>
#include <Library/PciLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/S3SaveState.h>
#include <Protocol/SmmControl2.h>

#include "SmiFeatures.h"

//
// Forward declaration.
//
STATIC
VOID
EFIAPI
OnS3SaveStateInstalled (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  );

//
// The absolute IO port address of the SMI Control and Enable Register. It is
// only used to carry information from the entry point function to the
// S3SaveState protocol installation callback, strictly before the runtime
// phase.
//
STATIC UINTN  mSmiEnable;

//
// Captures whether SMI feature negotiation is supported. The variable is only
// used to carry this information from the entry point function to the
// S3SaveState protocol installation callback.
//
STATIC BOOLEAN  mSmiFeatureNegotiation;

//
// Event signaled when an S3SaveState protocol interface is installed.
//
STATIC EFI_EVENT  mS3SaveStateInstalled;

/**
  Invokes SMI activation from either the preboot or runtime environment.

  This function generates an SMI.

  @param[in]     This                The EFI_SMM_CONTROL2_PROTOCOL instance.
  @param[in,out] CommandPort         The value written to the command port.
  @param[in,out] DataPort            The value written to the data port.
  @param[in]     Periodic            Optional mechanism to engender a periodic
                                     stream.
  @param[in]     ActivationInterval  Optional parameter to repeat at this
                                     period one time or, if the Periodic
                                     Boolean is set, periodically.

  @retval EFI_SUCCESS            The SMI/PMI has been engendered.
  @retval EFI_DEVICE_ERROR       The timing is unsupported.
  @retval EFI_INVALID_PARAMETER  The activation period is unsupported.
  @retval EFI_INVALID_PARAMETER  The last periodic activation has not been
                                 cleared.
  @retval EFI_NOT_STARTED        The SMM base service has not been initialized.
**/
STATIC
EFI_STATUS
EFIAPI
SmmControl2DxeTrigger (
  IN CONST EFI_SMM_CONTROL2_PROTOCOL  *This,
  IN OUT UINT8                        *CommandPort       OPTIONAL,
  IN OUT UINT8                        *DataPort          OPTIONAL,
  IN BOOLEAN                          Periodic           OPTIONAL,
  IN UINTN                            ActivationInterval OPTIONAL
  )
{
  //
  // No support for queued or periodic activation.
  //
  if (Periodic || (ActivationInterval > 0)) {
    return EFI_DEVICE_ERROR;
  }

  //
  // The so-called "Advanced Power Management Status Port Register" is in fact
  // a generic data passing register, between the caller and the SMI
  // dispatcher. The ICH9 spec calls it "scratchpad register" --  calling it
  // "status" elsewhere seems quite the misnomer. Status registers usually
  // report about hardware status, while this register is fully governed by
  // software.
  //
  // Write to the status register first, as this won't trigger the SMI just
  // yet. Then write to the control register.
  //
  IoWrite8 (ICH9_APM_STS, DataPort    == NULL ? 0 : *DataPort);
  IoWrite8 (ICH9_APM_CNT, CommandPort == NULL ? 0 : *CommandPort);
  return EFI_SUCCESS;
}

/**
  Clears any system state that was created in response to the Trigger() call.

  This function acknowledges and causes the deassertion of the SMI activation
  source.

  @param[in] This                The EFI_SMM_CONTROL2_PROTOCOL instance.
  @param[in] Periodic            Optional parameter to repeat at this period
                                 one time

  @retval EFI_SUCCESS            The SMI/PMI has been engendered.
  @retval EFI_DEVICE_ERROR       The source could not be cleared.
  @retval EFI_INVALID_PARAMETER  The service did not support the Periodic input
                                 argument.
**/
STATIC
EFI_STATUS
EFIAPI
SmmControl2DxeClear (
  IN CONST EFI_SMM_CONTROL2_PROTOCOL  *This,
  IN BOOLEAN                          Periodic OPTIONAL
  )
{
  if (Periodic) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The PI spec v1.4 explains that Clear() is only supposed to clear software
  // status; it is not in fact responsible for deasserting the SMI. It gives
  // two reasons for this: (a) many boards clear the SMI automatically when
  // entering SMM, (b) if Clear() actually deasserted the SMI, then it could
  // incorrectly suppress an SMI that was asynchronously asserted between the
  // last return of the SMI handler and the call made to Clear().
  //
  // In fact QEMU automatically deasserts CPU_INTERRUPT_SMI in:
  // - x86_cpu_exec_interrupt() [target-i386/seg_helper.c], and
  // - kvm_arch_pre_run() [target-i386/kvm.c].
  //
  // So, nothing to do here.
  //
  return EFI_SUCCESS;
}

STATIC EFI_SMM_CONTROL2_PROTOCOL  mControl2 = {
  &SmmControl2DxeTrigger,
  &SmmControl2DxeClear,
  MAX_UINTN // MinimumTriggerPeriod -- we don't support periodic SMIs
};

//
// Entry point of this driver.
//
EFI_STATUS
EFIAPI
SmmControl2DxeEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  UINT32      PmBase;
  UINT32      SmiEnableVal;
  EFI_STATUS  Status;

  //
  // This module should only be included if SMRAM support is required.
  //
  ASSERT (FeaturePcdGet (PcdSmmSmramRequire));

  //
  // Calculate the absolute IO port address of the SMI Control and Enable
  // Register. (As noted at the top, the PEI phase has left us with a working
  // ACPI PM IO space.)
  //
  PmBase = PciRead32 (POWER_MGMT_REGISTER_Q35 (ICH9_PMBASE)) &
           ICH9_PMBASE_MASK;
  mSmiEnable = PmBase + ICH9_PMBASE_OFS_SMI_EN;

  //
  // If APMC_EN is pre-set in SMI_EN, that's QEMU's way to tell us that SMI
  // support is not available. (For example due to KVM lacking it.) Otherwise,
  // this bit is clear after each reset.
  //
  SmiEnableVal = IoRead32 (mSmiEnable);
  if (!FeaturePcdGet (PcdStandaloneMmEnable)) {
    if ((SmiEnableVal & ICH9_SMI_EN_APMC_EN) != 0) {
      DEBUG ((
        DEBUG_ERROR,
        "%a: this Q35 implementation lacks SMI\n",
        __func__
        ));
      goto FatalError;
    }
  }

  //
  // Otherwise, configure the board to inject an SMI when ICH9_APM_CNT is
  // written to. (See the Trigger() method above.)
  //
  SmiEnableVal |= ICH9_SMI_EN_APMC_EN | ICH9_SMI_EN_GBL_SMI_EN;
  IoWrite32 (mSmiEnable, SmiEnableVal);

  //
  // Prevent software from undoing the above (until platform reset).
  //
  PciOr16 (
    POWER_MGMT_REGISTER_Q35 (ICH9_GEN_PMCON_1),
    ICH9_GEN_PMCON_1_SMI_LOCK
    );

  //
  // If we can clear GBL_SMI_EN now, that means QEMU's SMI support is not
  // appropriate.
  //
  IoWrite32 (mSmiEnable, SmiEnableVal & ~(UINT32)ICH9_SMI_EN_GBL_SMI_EN);
  if (IoRead32 (mSmiEnable) != SmiEnableVal) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: failed to lock down GBL_SMI_EN\n",
      __func__
      ));
    goto FatalError;
  }

  //
  // QEMU can inject SMIs in different ways, negotiate our preferences.
  //
  mSmiFeatureNegotiation = NegotiateSmiFeatures ();

  if (PcdGetBool (PcdAcpiS3Enable)) {
    VOID  *Registration;

    //
    // On S3 resume the above register settings have to be repeated. Register a
    // protocol notify callback that, when boot script saving becomes
    // available, saves operations equivalent to the above to the boot script.
    //
    Status = gBS->CreateEvent (
                    EVT_NOTIFY_SIGNAL,
                    TPL_CALLBACK,
                    OnS3SaveStateInstalled,
                    NULL /* Context */,
                    &mS3SaveStateInstalled
                    );
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a: CreateEvent: %r\n", __func__, Status));
      goto FatalError;
    }

    Status = gBS->RegisterProtocolNotify (
                    &gEfiS3SaveStateProtocolGuid,
                    mS3SaveStateInstalled,
                    &Registration
                    );
    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "%a: RegisterProtocolNotify: %r\n",
        __func__,
        Status
        ));
      goto ReleaseEvent;
    }

    //
    // Kick the event right now -- maybe the boot script is already saveable.
    //
    Status = gBS->SignalEvent (mS3SaveStateInstalled);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a: SignalEvent: %r\n", __func__, Status));
      goto ReleaseEvent;
    }
  }

  //
  // We have no pointers to convert to virtual addresses. The handle itself
  // doesn't matter, as protocol services are not accessible at runtime.
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                  &ImageHandle,
                  &gEfiSmmControl2ProtocolGuid,
                  &mControl2,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: InstallMultipleProtocolInterfaces: %r\n",
      __func__,
      Status
      ));
    goto ReleaseEvent;
  }

  return EFI_SUCCESS;

ReleaseEvent:
  if (mS3SaveStateInstalled != NULL) {
    gBS->CloseEvent (mS3SaveStateInstalled);
  }

FatalError:
  //
  // We really don't want to continue in this case.
  //
  ASSERT (FALSE);
  CpuDeadLoop ();
  return EFI_UNSUPPORTED;
}

/**
  Notification callback for S3SaveState installation.

  @param[in] Event    Event whose notification function is being invoked.

  @param[in] Context  The pointer to the notification function's context, which
                      is implementation-dependent.
**/
STATIC
VOID
EFIAPI
OnS3SaveStateInstalled (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  EFI_STATUS                  Status;
  EFI_S3_SAVE_STATE_PROTOCOL  *S3SaveState;
  UINT32                      SmiEnOrMask, SmiEnAndMask;
  UINT64                      GenPmCon1Address;
  UINT16                      GenPmCon1OrMask, GenPmCon1AndMask;

  ASSERT (Event == mS3SaveStateInstalled);

  Status = gBS->LocateProtocol (
                  &gEfiS3SaveStateProtocolGuid,
                  NULL /* Registration */,
                  (VOID **)&S3SaveState
                  );
  if (EFI_ERROR (Status)) {
    return;
  }

  //
  // These operations were originally done, verified and explained in the entry
  // point function of the driver.
  //
  SmiEnOrMask  = ICH9_SMI_EN_APMC_EN | ICH9_SMI_EN_GBL_SMI_EN;
  SmiEnAndMask = MAX_UINT32;
  Status       = S3SaveState->Write (
                                S3SaveState,
                                EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE,
                                EfiBootScriptWidthUint32,
                                (UINT64)mSmiEnable,
                                &SmiEnOrMask,
                                &SmiEnAndMask
                                );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: EFI_BOOT_SCRIPT_IO_READ_WRITE_OPCODE: %r\n",
      __func__,
      Status
      ));
    ASSERT (FALSE);
    CpuDeadLoop ();
  }

  GenPmCon1Address = POWER_MGMT_REGISTER_Q35_EFI_PCI_ADDRESS (
                       ICH9_GEN_PMCON_1
                       );
  GenPmCon1OrMask  = ICH9_GEN_PMCON_1_SMI_LOCK;
  GenPmCon1AndMask = MAX_UINT16;
  Status           = S3SaveState->Write (
                                    S3SaveState,
                                    EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE,
                                    EfiBootScriptWidthUint16,
                                    GenPmCon1Address,
                                    &GenPmCon1OrMask,
                                    &GenPmCon1AndMask
                                    );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: EFI_BOOT_SCRIPT_PCI_CONFIG_READ_WRITE_OPCODE: %r\n",
      __func__,
      Status
      ));
    ASSERT (FALSE);
    CpuDeadLoop ();
  }

  DEBUG ((DEBUG_VERBOSE, "%a: chipset boot script saved\n", __func__));

  //
  // Append a boot script fragment that re-selects the negotiated SMI features.
  //
  if (mSmiFeatureNegotiation) {
    SaveSmiFeatures ();
  }

  gBS->CloseEvent (Event);
  mS3SaveStateInstalled = NULL;
}
